Compare commits

..

3 Commits

Author SHA1 Message Date
ridiculousfish 6735e6e035 Add cirrus.yml
This adds a CI job script for cirrus-ci.com.
2022-11-12 13:48:25 -08:00
ridiculousfish 7a3771bdbe Increase debounce timeout in debounce test
On slow machines this was spuriously failing.
2022-11-12 13:26:40 -08:00
ridiculousfish d65192e3c5 Reduce FISH_MAX_EVAL_DEPTH under tsan
The stack overflow tests are too slow without this.
This is because the tests are essentially quadratic: with 500 jobs, and
each job attempts to reap all jobs.
2022-11-12 10:15:23 -08:00
447 changed files with 318959 additions and 587100 deletions

View File

@ -51,15 +51,11 @@ linux_task:
- ninja -j 6 fish fish_tests
- ninja fish_run_tests
# CI task disabled during RIIR transition
only_if: false && $CIRRUS_REPO_OWNER == 'fish-shell'
linux_arm_task:
matrix:
- name: focal-arm64
arm_container:
image: ghcr.io/fish-shell/fish-ci/focal-arm64
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'
- name: jammy-armv7-32bit
arm_container:
image: ghcr.io/fish-shell/fish-ci/jammy-armv7-32bit
@ -75,24 +71,19 @@ linux_arm_task:
- file ./fish
- ninja fish_run_tests
# CI task disabled during RIIR transition
only_if: false && $CIRRUS_REPO_OWNER == 'fish-shell'
freebsd_task:
matrix:
# - name: FreeBSD 14
# freebsd_instance:
# image_family: freebsd-14-0-snap
- name: FreeBSD 14
freebsd_instance:
image_family: freebsd-14-0-snap
- name: FreeBSD 13
freebsd_instance:
image: freebsd-13-1-release-amd64
image: freebsd-13-0-release-amd64
- name: FreeBSD 12.3
freebsd_instance:
image: freebsd-12-3-release-amd64
tests_script:
- pkg install -y cmake-core devel/pcre2 devel/ninja misc/py-pexpect git-lite
# libclang.so is a required build dependency for rust-c++ ffi bridge
- pkg install -y llvm
- pkg install -y cmake devel/pcre2 devel/ninja misc/py-pexpect git
# BSDs have the following behavior: root may open or access files even if
# the mode bits would otherwise disallow it. For example root may open()
# a file with write privileges even if the file has mode 400. This breaks
@ -103,16 +94,6 @@ freebsd_task:
- mkdir build && cd build
- chown -R fish-user ..
- sudo -u fish-user -s whoami
# FreeBSD's pkg currently has rust 1.66.0 while we need rust 1.67.0+. Use rustup to install
# the latest, but note that it only installs rust per-user.
- sudo -u fish-user -s fetch -qo - https://sh.rustup.rs > rustup.sh
- sudo -u fish-user -s sh ./rustup.sh -y --profile=minimal
# `sudo -s ...` does not invoke a login shell so we need a workaround to make sure the
# rustup environment is configured for subsequent `sudo -s ...` commands.
# For some reason, this doesn't do the job:
# - sudo -u fish-user sh -c 'echo source \$HOME/.cargo/env >> $HOME/.cshrc'
- sudo -u fish-user -s cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCTEST_PARALLEL_LEVEL=1 ..
- sudo -u fish-user sh -c '. $HOME/.cargo/env; ninja -j 6 fish fish_tests'
- sudo -u fish-user sh -c '. $HOME/.cargo/env; ninja fish_run_tests'
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'
- sudo -u fish-user -s ninja -j 6 fish fish_tests
- sudo -u fish-user -s ninja fish_run_tests

View File

@ -17,9 +17,9 @@ jobs:
pull-requests: write # for dessant/lock-threads to lock PRs
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-inactive-days: '365'
pr-inactive-days: '365'
exclude-any-issue-labels: 'question, needs more info'
issue-lock-inactive-days: '365'
pr-lock-inactive-days: '365'
issue-exclude-labels: 'question, needs more info'

View File

@ -15,11 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: SetupRust
uses: ATiltedTree/setup-rust@v1
with:
rust-version: 1.67
- uses: actions/checkout@v2
- name: Install deps
run: |
sudo apt install gettext libncurses5-dev libpcre2-dev python3-pip tmux
@ -45,12 +41,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: SetupRust
uses: ATiltedTree/setup-rust@v1
with:
rust-version: 1.67
targets: "i686-unknown-linux-gnu" # setup-rust wants this space-separated
- uses: actions/checkout@v2
- name: Install deps
run: |
sudo apt update
@ -62,10 +53,10 @@ jobs:
CFLAGS: "-m32"
run: |
mkdir build && cd build
cmake -DFISH_USE_SYSTEM_PCRE2=OFF -DRust_CARGO_TARGET=i686-unknown-linux-gnu ..
cmake -DFISH_USE_SYSTEM_PCRE2=OFF ..
- name: make
run: |
make VERBOSE=1
make
- name: make test
run: |
make test
@ -73,24 +64,9 @@ jobs:
ubuntu-asan:
runs-on: ubuntu-latest
env:
# Rust has two different memory sanitizers of interest; they can't be used at the same time:
# * AddressSanitizer detects out-of-bound access, use-after-free, use-after-return,
# use-after-scope, double-free, invalid-free, and memory leaks.
# * MemorySanitizer detects uninitialized reads.
#
RUSTFLAGS: "-Zsanitizer=address"
# RUSTFLAGS: "-Zsanitizer=memory -Zsanitizer-memory-track-origins"
steps:
- uses: actions/checkout@v3
- name: SetupRust
uses: ATiltedTree/setup-rust@v1
with:
# All -Z options require running nightly
rust-version: nightly
# ASAN uses `cargo build -Zbuild-std` which requires the rust-src component
components: rust-src
- uses: actions/checkout@v2
- name: Install deps
run: |
sudo apt install gettext libncurses5-dev libpcre2-dev python3-pip tmux
@ -102,15 +78,12 @@ jobs:
CXXFLAGS: "-fno-omit-frame-pointer -fsanitize=undefined -fsanitize=address -DFISH_CI_SAN"
run: |
mkdir build && cd build
# Rust's ASAN requires the build system to explicitly pass a --target triple. We read that
# value from CMake variable Rust_CARGO_TARGET (shared with corrosion).
cmake .. -DASAN=1 -DRust_CARGO_TARGET=x86_64-unknown-linux-gnu -DCMAKE_BUILD_TYPE=Debug
cmake ..
- name: make
run: |
make
- name: make test
env:
FISH_CI_SAN: 1
ASAN_OPTIONS: check_initialization_order=1:detect_stack_use_after_return=1:detect_leaks=1
UBSAN_OPTIONS: print_stacktrace=1:report_error_type=1
# use_tls=0 is a workaround for LSAN crashing with "Tracer caught signal 11" (SIGSEGV),
@ -121,50 +94,37 @@ jobs:
run: |
make test
# Our clang++ tsan builds are not recognizing safe rust patterns (such as the fact that Drop
# cannot be called while a thread is using the object in question). Rust has its own way of
# running TSAN, but for the duration of the port from C++ to Rust, we'll keep this disabled.
ubuntu-threadsan:
# ubuntu-threadsan:
#
# runs-on: ubuntu-latest
#
# steps:
# - uses: actions/checkout@v3
# - name: SetupRust
# uses: ATiltedTree/setup-rust@v1
# with:
# rust-version: 1.67
# - name: Install deps
# run: |
# sudo apt install gettext libncurses5-dev libpcre2-dev python3-pip tmux
# sudo pip3 install pexpect
# - name: cmake
# env:
# FISH_CI_SAN: 1
# CC: clang
# CXX: clang++
# CXXFLAGS: "-fsanitize=thread"
# run: |
# mkdir build && cd build
# cmake ..
# - name: make
# run: |
# make
# - name: make test
# run: |
# make test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install deps
run: |
sudo apt install gettext libncurses5-dev libpcre2-dev python3-pip tmux
sudo pip3 install pexpect
- name: cmake
env:
CC: clang
CXX: clang++
CXXFLAGS: "-fsanitize=thread"
run: |
mkdir build && cd build
cmake ..
- name: make
run: |
make
- name: make test
run: |
make test
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: SetupRust
uses: ATiltedTree/setup-rust@v1
with:
rust-version: 1.67
- uses: actions/checkout@v2
- name: Install deps
run: |
sudo pip3 install pexpect

View File

@ -1,42 +0,0 @@
name: Rust checks
on: [push, pull_request]
permissions:
contents: read
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: SetupRust
uses: ATiltedTree/setup-rust@v1
with:
rust-version: stable
- name: cargo fmt
run: |
cd fish-rust
cargo fmt --check --all
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: SetupRust
uses: ATiltedTree/setup-rust@v1
with:
rust-version: stable
- name: Install deps
run: |
sudo apt install gettext libncurses5-dev libpcre2-dev python3-pip tmux
sudo pip3 install pexpect
- name: cmake
run: |
cmake -B build
- name: cargo clippy
run: |
cd fish-rust
cargo clippy --workspace --all-targets -- --deny=warnings

13
.gitignore vendored
View File

@ -89,16 +89,3 @@ __pycache__
/tags
xcuserdata/
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Generated by clangd
/.cache

View File

@ -1,106 +1,12 @@
fish 3.7.0 (released ???)
.. ignore: 2271 9265 9252 8514 9241 9226 9214 9211 9206 9186 9099 9154 9152 9141 7717 9140 9134 9121 9111 9109 9091 9067 9028
fish 3.6.0 (released ???)
===================================
.. ignore: 9439 9440 9442 9452 9469 9480 9482
Notable improvements and fixes
------------------------------
- ``abbr --erase`` now also erases the universal variables used by the old abbr function. That means::
abbr --erase (abbr --list)
can now be used to clean out all old abbreviations (:issue:`9468`).
- ``abbr --add --universal`` now warns about --universal being non-functional, to make it easier to detect old-style ``abbr`` calls (:issue:`9475`).
- ``functions --handlers-type caller-exit`` once again lists functions defined as ``function --on-job-exit caller``, rather than them being listed by ``functions --handlers-type process-exit``.
Deprecations and removed features
---------------------------------
Scripting improvements
----------------------
- ``abbr --list`` no longer escapes the abbr name, which is necessary to be able to pass it to ``abbr --erase`` (:issue:`9470`).
- ``read`` will now print an error if told to set a read-only variable instead of silently doing nothing (:issue:`9346`).
- ``functions`` and ``type`` now show where a function was copied and where it originally was instead of saying ``Defined interactively``.
- Stack trace now shows line numbers for copied functions.
Interactive improvements
------------------------
- Using ``fish_vi_key_bindings`` in combination with fish's ``--no-config`` mode works without locking up the shell (:issue:`9443`).
- The history pager now uses more screen space, usually half the screen (:issue:`9458`).
- The history pager now shows fuzzy (subsequence) matches in the absence of exact substring matches (:issue:`9476`).
- Variables that were set while the locale was C (i.e. ASCII) will now properly be encoded if the locale is switched (:issue:`2613`, :issue:`9473`).
- Escape during history search restores the original commandline again (regressed in 3.6.0).
- Using ``--help`` on builtins now respects the $MANPAGER variable in preference to $PAGER (:issue:`9488`).
- Command-specific tab completions may now offer results whose first character is a period. For example, it is now possible to tab-complete ``git add`` for files with leading periods. The default file completions hide these files, unless the token itself has a leading period (:issue:`3707`).
- A new variable, :envvar:`fish_cursor_external`, can be used to specify to cursor shape when a command is launched. When unspecified, the value defaults to the value of :envvar:`fish_cursor_default` (:issue:`4656`).
New or improved bindings
^^^^^^^^^^^^^^^^^^^^^^^^
Improved prompts
^^^^^^^^^^^^^^^^
Completions
^^^^^^^^^^^
- Added completions for:
- ``otool``
- ``mix phx``
- ``neovim``
- ``stow``
- ``trash`` and helper utilities ``trash-empty``, ``trash-list``, ``trash-put``, ``trash-restore``
- ``apkanalyzer``
- ``scrypt``
- ``fastboot``
- git's completion for ``git-foo``-style commands was fixed (:issue:`9457`)
- File completion now offers ``../`` and ``./`` again (:issue:`9477`)
Improved terminal support
^^^^^^^^^^^^^^^^^^^^^^^^^
Other improvements
------------------
For distributors
----------------
- *Placeholder to fix Sphinx warning*
--------------
fish 3.6.0 (released January 7, 2023)
=====================================
Notable improvements and fixes
------------------------------
- By default, :kbd:`Control-R` now opens the command history in the pager (:issue:`602`). This is fully searchable and syntax-highlighted, as an alternative to the incremental search seen in other shells. The new special input function ``history-pager`` has been added for custom bindings.
- Abbrevations are more flexible (:issue:`9313`, :issue:`5003`, :issue:`2287`):
- They may optionally replace tokens anywhere on the command line, instead of only commands
- Matching tokens may be described using a regular expression instead of a literal word
- The replacement text may be produced by a fish function, instead of a literal word
- They may position the cursor anywhere in the expansion, instead of at the end
For example::
function multicd
echo cd (string repeat -n (math (string length -- $argv[1]) - 1) ../)
end
abbr --add dotdot --regex '^\.\.+$' --function multicd
This expands ``..`` to ``cd ../``, ``...`` to ``cd ../../`` and ``....`` to ``cd ../../../`` and so on.
Or::
function last_history_item; echo $history[1]; end
abbr -a !! --position anywhere --function last_history_item
which expands ``!!`` to the last history item, anywhere on the command line, mimicking other shells' history expansion.
See :ref:`the documentation <cmd-abbr>` for more.
- ``path`` gained a new ``mtime`` subcommand to print the modification time stamp for files. For example, this can be used to handle cache file ages (:issue:`9057`)::
- By default, :kbd:``Control-R`` now opens the command history in the pager, via the new special input function ``history-pager`` (:issue:`9089`, :issue:`602`). This is fully searchable and syntax-highlighted, as an alternative to the "isearch" seen in other shells.
- ``path`` gained a new ``mtime`` command to print the modification time stamp for files. This can be used e.g. to handle cache file ages (:issue:`9057`)::
> touch foo
> sleep 10
@ -109,30 +15,31 @@ Notable improvements and fixes
- ``string`` gained a new ``shorten`` subcommand to shorten strings to a given visible width (:issue:`9156`)::
> string shorten --max 10 "Hello this is a long string"
> string shorten -m10 "Hello this is a long string"
Hello thi…
- ``test`` (aka ``[``) gained ``-ot`` (older than) and ``-nt`` (newer than) operators to compare file modification times, and ``-ef`` to compare whether the arguments are the same file (:issue:`3589`).
- fish will now mark the extent of many errors with a squiggly line, instead of just a caret (``^``) at the beginning (:issue:`9130`). For example::
- ``test`` aka ``[``: implemented ``-ot`` (older than) and ``-nt`` (newer than) operators to compare file modification times, and ``-ef`` to compare identity, common extensions (:issue:`3589`).
- Fish will now mark the extent of many errors with a squiggly line instead of just a caret (``^``) at the beginning (:issue:`9130`). For example::
checks/set.fish (line 471): for: a,b: invalid variable name. See `help identifiers`
for a,b in y 1 z 3
^~^
- A new function, ``fish_delta``, shows changes that have been made in fish's configuration from the defaults (:issue:`9255`).
- ``set --erase`` can now be used with multiple scopes at once, like ``set -efglU foo`` (:issue:`7711`, :issue:`9280`).
- ``status`` gained a new subcommand, ``current-commandline``, which retrieves the entirety of the currently-executing command line when called from a function during execution. This allows easier job introspection (:issue:`8905`, :issue:`9296`).
- A new helper function ``fish_delta`` can be used to show the difference to fish's stock configuration (:issue:`9255`).
- It is now possible to specify multiple scopes for ``set -e`` and all of the named variables present in any of the specified scopes will be erased. This makes it possible to remove all instances of a variable in all scopes (``set -efglU foo``) in one go (:issue:`7711`).
- A possible stack overflow when recursively evaluating substitutions has been fixed (:issue:`9302`).
- `status current-commandline` has been added and retrieves the entirety of the currently executing commandline when called from a function during execution, allowing easier job introspection (:issue:`8905`).
=======
Deprecations and removed features
---------------------------------
- The ``\x`` and ``\X`` escape syntax is now equivalent. ``\xAB`` previously behaved the same as ``\XAB``, except that it would error if the value "AB" was larger than "7f" (127 in decimal, the highest ASCII value) (:issue:`9247`, :issue:`9245`, :issue:`1352`).
- The ``fish_git_prompt`` will now only turn on features if the appropriate variable has been set to a true value (of "1", "yes" or "true") instead of just checking if it is defined. This allows specifically turning features *off* without having to erase variables, such as via universal variables. If you have defined a variable to a different value and expect it to count as true, you need to change it (:issue:`9274`).
For example, ``set -g __fish_git_prompt_show_informative_status 0`` previously would have enabled informative status (because any value would have done so), but now it turns it off.
- Abbreviations are no longer stored in universal variables. Existing universal abbreviations are still imported, but new abbreviations should be added to ``config.fish``.
- The short option ``-r`` for abbreviations has changed from ``rename`` to ``regex``, for consistency with ``string``.
- The difference between ``\xAB`` and ``\XAB`` has been removed. Before, ``\x`` would do the same thing as ``\X`` except that it would error if the value was larger than "7f" (127 in decimal, the highest ASCII value) (:issue:`9247`, :issue:`1352`).
- The ``fish_git_prompt`` will now only turn on features if the corresponding boolean variable has been set to a true value (of "1", "yes" or "true") instead of just checking if it is defined. This allows specifically turning features *off* without having to erase variables, e.g. via universal variables. If you have defined a variable to a different value and expect it to count as true, you need to change it (:issue:`9274`).
For example, ``set -g __fish_git_prompt_show_informative_status 0`` previously would have enabled informative status (because any value would have done so), now it turns it off.
Scripting improvements
----------------------
- ``argparse`` can now be used without option specifications, to allow using ``--min-args``, ``--max-args`` or for commands that take no options (but might in future) (:issue:`9006`)::
- ``argparse`` can now be used without option specifications, to allow using --min-args, --max-args or for commands that take no options (but might in future) (:issue:`9006`)::
function my_copy
argparse --min-args 2 -- $argv
@ -151,118 +58,79 @@ Scripting improvements
$XDG_DATA_DIRS[4]: |/usr/share|
$XDG_DATA_DIRS: originally inherited as |/home/alfa/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share/:/usr/share/|
- The read limit is now restored to the default when :envvar:`fish_read_limit` is unset (:issue:`9129`).
- ``math`` produces an error for division-by-zero, as well as augmenting some errors with their extent (:issue:`9190`). This changes behavior in some limited cases, such as::
- The read limit is now restored to the default when $fish_read_limit is unset (:issue:`9129`).
- ``math`` now actually handles division-by-zero while computing and gives it a bespoke error, as well as augmenting some errors with their extent (:issue:`9190`). This changes behavior in some limited cases. E.g.::
math min 1 / 0, 5
which would previously print "5" (because in floating point division "1 / 0" yields infinite, and 5 is smaller than infinite) but will now return an error.
would previously print "5" because in floating point division "1 / 0" yields infinite, and 5 is smaller than infinite. Instead, ``math`` will now error out.
- ``string`` is now faster when reading large strings from stdin (:issue:`9139`).
- ``string repeat`` no longer allocates the entire output at once, instead using chunks. This needs less memory and has less of a delay with long strings. Also it was possible to make fish crash by making it allocate more memory than the system had. (:issue:`9124`)
- Builtins writing to a pipe or file was optimized. In particular printf no longer issues a separate ``write()`` syscall for every escaped character. (:issue:`9229`).
- ``fish_clipboard_copy`` and ``fish_clipboard_paste`` can now be used in pipes (:issue:`9271`)::
git rev-list 3.5.1 | fish_clipboard_copy
fish_clipboard_paste | string join + | math
- ``status fish-path`` returns a fully-normalised path, particularly noticeable on NetBSD (:issue:`9085`).
Interactive improvements
------------------------
- If the terminal definition for :envvar:`TERM` can't be found, fish now tries using the "xterm-256color" and "xterm" definitions before "ansi" and "dumb". As the majority of terminal emulators in common use are now more or less xterm-compatible (often even explicitly claiming the xterm-256color entry), this should often result in a fully or almost fully usable terminal (:issue:`9026`).
- A new variable, :envvar:`fish_cursor_selection_mode`, can be used to configure whether the command line selection includes the character under the cursor (``inclusive``) or not (``exclusive``). The new default is ``exclusive``; use ``set fish_cursor_selection_mode inclusive`` to get the previous behavior back (:issue:`7762`).
- fish's completion pager now fills half the terminal on first tab press instead of only 4 rows, which should make results visible more often and save key presses, without constantly snapping fish to the top of the terminal (:issue:`9105`, :issue:`2698`).
- The ``complete-and-search`` binding, used with :kbd:`Shift-Tab` by default, selects the first item in the results immediately (:issue:`9080`).
- If the terminal definition for $TERM can't be used, fish now tries using the "xterm-256color" and "xterm" definitions before "ansi" and "dumb". As the majority of terminal emulators in common use are now more or less xterm-compatible (often even explicitly claiming the xterm-256color entry), this should often result in a fully or almost fully usable terminal (:issue:`9026`).
- The new environment variable ``$fish_cursor_selection_mode`` can be used to configure whether the command line selection includes (``inclusive``) the character under the cursor or not (``exclusive``). The new default is ``exclusive``. Use ``set fish_cursor_selection_mode inclusive`` to get the previous behavior back (:issue:`7762`).
- Fish's completion pager now fills half the terminal on first tab press instead of only 4 rows, which should make results visible more often and save key presses, without constantly snapping fish to the top of the terminal (:issue:`9105`, :issue:`2698`).
- ``bind`` output is now syntax-highlighted when used interacively.
- :kbd:`Alt-H` (the default ``__fish_man_page`` binding) does a better job of showing the manual page of the command under cursor (:issue:`9020`).
- If :envvar:`fish_color_valid_path` contains an actual color instead of just modifiers, those will be used for valid paths even if the underlying color isn't "normal" (:issue:`9159`).
- The key combination for the QUIT terminal sequence, often :kbd:`Control-Backslash` (``\x1c``), can now be sused as a binding (:issue:`9234`).
- fish's vi mode uses normal xterm-style sequences to signal cursor change, instead of using the iTerm's proprietary escape sequences. This allows for a blinking cursor and makes it work in complicated scenarios with nested terminals. (:issue:`3741`, :issue:`9172`)
- When running fish on a remote system (such as inside SSH or a container), :kbd:`Control-X` now copies to the local client system's clipboard if the terminal supports OSC 52.
- ``commandline`` gained two new options, ``--selection-start`` and ``--selection-end``, to set the start/end of the current selection (:issue:`9197`, :issue:`9215`).
- fish's builtins now handle keyboard interrupts (:kbd:`Control-C`) correctly (:issue:`9266`).
- The :kbd:`Alt-H` binding will now show the manpage of the command under cursor instead of the always skipping ``sudo`` and the likes (:issue:`9020`).
- If ``$fish_color_valid_path`` contains an actual color instead of just modifiers, those will be used for valid paths even if the underlying color isn't "normal" (:issue:`9159`).
- Performance improvements to highlighting (:issue:`9180`) and the cd completions (:issue:`9220`) should make using fish more pleasant on slow systems.
- Fish now disables the QUIT terminal sequence when it has the terminal. This frees up a key combination, often ctrl-backslash (``\x1c``) (:issue:`9234`).
- Fish's vi mode no longer uses iTerm's proprietary escape sequences to signal cursor change, instead using the normal xterm-style sequences. This allows for a blinking cursor and makes it work in complicated scenarios with nested terminals. (:issue:`3741`, :issue:`9172`)
- Generating descriptions for commands now uses ``manpath`` instead of ``man --path`` on macOS, as that has been removed in macOS Ventura.
- When running fish on a remote system (e.g. inside SSH or a container), :kbd:`Control-X` now copies to the local client system's clipboard if the terminal supports OSC 52.
Fixed Bugs
----------
- The history search text for a token search is now highlighted correctly if the line contains multiple instances of that text.
- process-exit and job-exit events are now generated for all background jobs, including those launched from event handlers (:issue:`9096`).
- A crash when completing a token that contained both a potential glob and a quoted variable expansion was fixed (:issue:`9137`).
- ``prompt_pwd`` no longer accidentally overwrites a global or universal ``$fish_prompt_pwd_full_dirs`` when called with the ``-d`` or ``--full-length-dirs`` option (:issue:`9123`).
- A bug which caused fish to freeze or exit after running a command which does not preserve the foreground process group was fixed (:issue:`9181`).
- The "Disco" sample prompt no longer prints an error in some working directories (:issue:`9164`). If you saved this prompt, you should run ``fish_config prompt save disco`` again.
- Fish calls external commands via the given path again instead of always making it absolute. This can be seen e.g. when you run a bash script and check ``$0`` (:issue:`9143`).
- ``printf`` no longer tries to interpret the first argument as an option (:issue:`9132`).
- On 32-bit systems, globs like ``*`` might fail to print certain files, due to missing large file support. This has been fixed by enabling large file support.
- Interactive ``read`` in scripts will now have the correct keybindings again (:issue:`9227`).
Completions
^^^^^^^^^^^
- Added completions for:
- ``ark``
- ``asciinema`` (:issue:`9257`)
- ``clojure`` (:issue:`9272`)
- ``csh``
- ``direnv`` (:issue:`9268`)
- ``dive`` (:issue:`9082`)
- ``dolphin``
- ``dua`` (:issue:`9277`)
- ``efivar`` (:issue:`9318`)
- ``eg``
- ``es`` (:issue:`9388`)
- ``firefox-developer-edition`` and ``firefox`` (:issue:`9090`)
- ``fortune`` (:issue:`9177`)
- ``kb``
- ``asciinema``
- ``firefox``
- ``firefox-developer-edition``
- ``fortune``
- ``kind`` (:issue:`9110`)
- ``konsole``
- ``ksh``
- ``loadkeys`` (:issue:`9312`)
- ``okular``
- ``op`` (:issue:`9300`)
- ``ouch`` (:issue:`9405`)
- ``pix``
- ``readelf`` (:issue:`8746`, :issue:`9386`)
- ``qshell``
- ``rc``
- ``sad`` (:issue:`9145`)
- ``tcsh``
- ``toot``
- ``tox`` (:issue:`9078`)
- ``wish``
- ``xed``
- ``xonsh`` (:issue:`9389`)
- ``xplayer``
- ``xreader``
- ``xviewer``
- ``yash`` (:issue:`9391`)
- ``zig`` (:issue:`9083`)
- ``sad``
- ``dua``
- ``clojure``
- ``loadkeys``
- Improvements to many completions, including making ``cd`` completion much faster (:issue:`9220`).
- Completion of tilde (``~``) works properly even when the file name contains an escaped character (:issue:`9073`).
- fish no longer loads completions if the command is used via a relative path and is not in :envvar:`PATH` (:issue:`9133`).
- fish no longer completes inside of comments (:issue:`9320`).
- Improvements to some completions.
Improved terminal support
^^^^^^^^^^^^^^^^^^^^^^^^^
- Opening ``help`` on WSL now uses PowerShell to open the browser if available, removing some awkward UNC path errors (:issue:`9119`).
Other improvements
------------------
- The Web-based configuration tool now works on systems with IPv6 disabled (:issue:`3857`).
- Aliases can ignore arguments by ending them with ``#`` (:issue:`9199`).
- ``string`` is now faster when reading large strings from stdin (:issue:`9139`).
- ``string repeat`` uses less memory and is faster. (:issue:`9124`)
- Builtins are much faster when writing to a pipe or file. (:issue:`9229`).
- Performance improvements to highlighting (:issue:`9180`) should make using fish more pleasant on slow systems.
- On 32-bit systems, globs like ``*`` will no longer fail to return some files, as large file support has been enabled.
- The css for fish's documentation no longer depends on sphinx' stock "classic" theme. This should improve compatibility with sphinx versions and ease upgrading (:issue:`9003`).
- The web-based configuration tool now works on systems that have ipv6 disabled (:issue:`3857`).
- Aliases can ignore arguments by ending them with ``#``.
Fixed bugs
----------
- The history search text for a token search is now highlighted correctly if the line contains multiple instances of that text (:issue:`9066`).
- ``process-exit`` and ``job-exit`` events are now generated for all background jobs, including those launched from event handlers (:issue:`9096`).
- A crash when completing a token that contained both a potential glob and a quoted variable expansion was fixed (:issue:`9137`).
- ``prompt_pwd`` no longer accidentally overwrites a global or universal ``$fish_prompt_pwd_full_dirs`` when called with the ``-d`` or ``--full-length-dirs`` option (:issue:`9123`).
- A bug which caused fish to freeze or exit after running a command which does not preserve the foreground process group was fixed (:issue:`9181`).
- The "Disco" sample prompt no longer prints an error in some working directories (:issue:`9164`). If you saved this prompt, you should run ``fish_config prompt save disco`` again.
- fish launches external commands via the given path again, rather than always using an absolute path. This behaviour was inadvertently changed in 3.5.0 and is visible, for example, when launching a bash script which checks ``$0`` (:issue:`9143`).
- ``printf`` no longer tries to interpret the first argument as an option (:issue:`9132`).
- Interactive ``read`` in scripts will now have the correct keybindings again (:issue:`9227`).
- A possible stack overflow when recursively evaluating substitutions has been fixed (:issue:`9302`).
- A crash with relative $CDPATH has been fixed (:issue:`9407`).
- ``printf`` now properly fills extra ``%d`` specifiers with 0 even on macOS and BSD (:issue:`9321`).
- ``fish_key_reader`` now correctly exits when receiving a SIGHUP (like after closing the terminal) (:issue:`9309`).
- ``fish_config theme save`` now works as documented instead of erroring out (:issue:`9088`, :issue:`9273`).
- fish no longer triggers prompts to install command line tools when first run on macOS (:issue:`9343`).
- ``fish_git_prompt`` now quietly fails on macOS if the xcrun cache is not yet populated (:issue:`6625`), working around a potential hang.
For distributors
----------------
- The vendored PCRE2 sources have been removed. It is recommended to declare PCRE2 as a dependency when packaging fish. If the CMake variable FISH_USE_SYSTEM_PCRE2 is false, fish will now download and build PCRE2 from the official repo (:issue:`8355`, :issue:`8363`). Note this variable defaults to true if PCRE2 is found installed on the system.
- The vendored PCRE2 sources have been removed. It is recommended to declare PCRE2 as a dependency when packaging fish. If the CMake variable FISH_USE_SYSTEM_PCRE2 is false, fish will now download and build PCRE2 from the official repo (:issue:`8355`). Note this variable defaults to true if PCRE2 is found installed on the system.
--------------
@ -337,7 +205,7 @@ Deprecations and removed features
This flag was introduced in fish 3.4.
To turn off these flags, add ``no-regex-easyesc`` or ``no-ampersand-nobg-in-token`` to :envvar:`fish_features` and restart fish::
To turn off these flags, add ``no-regex-easyesc`` or ``no-ampersand-nobg-in-token`` to :envvar:`fish_features`` and restart fish::
set -Ua fish_features no-regex-easyesc

View File

@ -24,8 +24,6 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}")
endif()
include(cmake/Rust.cmake)
# Error out when linking statically, it doesn't work.
if (CMAKE_EXE_LINKER_FLAGS MATCHES ".*-static.*")
message(FATAL_ERROR "Fish does not support static linking")
@ -45,9 +43,6 @@ endif()
# - address, because that occurs for our mkostemp check (weak-linking requires us to compare `&mkostemp == nullptr`).
add_compile_options(-Wall -Wextra -Wno-comment -Wno-address)
# Get extra C++ files from Rust.
get_property(FISH_EXTRA_SOURCES TARGET fish-rust PROPERTY fish_extra_cpp_files)
if ((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") OR (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang"))
add_compile_options(-Wunused-template -Wunused-local-typedef -Wunused-macros)
endif()
@ -58,9 +53,6 @@ add_compile_options(-fno-exceptions)
# Undefine NDEBUG to keep assert() in release builds.
add_definitions(-UNDEBUG)
# Allow including Rust headers in normal (not bindgen) builds.
add_definitions(-DINCLUDE_RUST_HEADERS)
# Enable large files on GNU.
add_definitions(-D_LARGEFILE_SOURCE
-D_LARGEFILE64_SOURCE
@ -99,34 +91,36 @@ endif()
# List of sources for builtin functions.
set(FISH_BUILTIN_SRCS
src/builtin.cpp src/builtins/argparse.cpp src/builtins/bind.cpp
src/builtin.cpp src/builtins/argparse.cpp
src/builtins/bg.cpp src/builtins/bind.cpp src/builtins/block.cpp
src/builtins/builtin.cpp src/builtins/cd.cpp src/builtins/command.cpp
src/builtins/commandline.cpp src/builtins/complete.cpp
src/builtins/disown.cpp
src/builtins/eval.cpp src/builtins/fg.cpp
src/builtins/commandline.cpp src/builtins/complete.cpp src/builtins/contains.cpp
src/builtins/disown.cpp src/builtins/echo.cpp src/builtins/emit.cpp
src/builtins/eval.cpp src/builtins/exit.cpp src/builtins/fg.cpp
src/builtins/function.cpp src/builtins/functions.cpp src/builtins/history.cpp
src/builtins/jobs.cpp src/builtins/math.cpp src/builtins/printf.cpp src/builtins/path.cpp
src/builtins/read.cpp src/builtins/set.cpp
src/builtins/pwd.cpp src/builtins/random.cpp src/builtins/read.cpp
src/builtins/realpath.cpp src/builtins/return.cpp src/builtins/set.cpp
src/builtins/set_color.cpp src/builtins/source.cpp src/builtins/status.cpp
src/builtins/string.cpp src/builtins/test.cpp src/builtins/type.cpp src/builtins/ulimit.cpp
)
src/builtins/wait.cpp)
# List of other sources.
set(FISH_SRCS
src/ast.cpp src/autoload.cpp src/color.cpp src/common.cpp src/complete.cpp
src/env.cpp src/env_dispatch.cpp src/env_universal_common.cpp src/event.cpp
src/exec.cpp src/expand.cpp src/fallback.cpp src/fish_version.cpp
src/flog.cpp src/function.cpp src/highlight.cpp
src/ast.cpp src/autoload.cpp src/color.cpp src/common.cpp src/complete.cpp src/env.cpp
src/env_dispatch.cpp src/env_universal_common.cpp src/event.cpp src/exec.cpp
src/expand.cpp src/fallback.cpp src/fd_monitor.cpp src/fish_version.cpp
src/flog.cpp src/function.cpp src/future_feature_flags.cpp src/highlight.cpp
src/history.cpp src/history_file.cpp src/input.cpp src/input_common.cpp
src/io.cpp src/iothread.cpp src/kill.cpp
src/io.cpp src/iothread.cpp src/job_group.cpp src/kill.cpp
src/null_terminated_array.cpp src/operation_context.cpp src/output.cpp
src/pager.cpp src/parse_execution.cpp src/parse_tree.cpp src/parse_util.cpp
src/parser.cpp src/parser_keywords.cpp src/path.cpp src/postfork.cpp
src/proc.cpp src/re.cpp src/reader.cpp src/screen.cpp
src/signals.cpp src/termsize.cpp src/tinyexpr.cpp
src/trace.cpp src/utf8.cpp
src/wait_handle.cpp src/wcstringutil.cpp src/wgetopt.cpp src/wildcard.cpp
src/wutil.cpp src/fds.cpp src/rustffi.cpp
src/proc.cpp src/re.cpp src/reader.cpp src/redirection.cpp src/screen.cpp
src/signal.cpp src/termsize.cpp src/timer.cpp src/tinyexpr.cpp
src/tokenizer.cpp src/topic_monitor.cpp src/trace.cpp src/utf8.cpp src/util.cpp
src/wait_handle.cpp src/wcstringutil.cpp src/wgetopt.cpp src/wildcard.cpp
src/wutil.cpp src/fds.cpp
)
# Header files are just globbed.
@ -139,11 +133,6 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config_cmake.h.in
${CMAKE_CURRENT_BINARY_DIR}/config.h)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
# Pull in our src directory for headers searches, but only quoted ones.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -iquote ${CMAKE_CURRENT_SOURCE_DIR}/src")
# Set up standard directories.
include(GNUInstallDirs)
add_definitions(-D_UNICODE=1
@ -186,10 +175,8 @@ endfunction(FISH_LINK_DEPS_AND_SIGN)
add_library(fishlib STATIC ${FISH_SRCS} ${FISH_BUILTIN_SRCS})
target_sources(fishlib PRIVATE ${FISH_HEADERS})
target_link_libraries(fishlib
fish-rust
${CURSES_LIBRARY} ${CURSES_EXTRA_LIBRARY} Threads::Threads ${CMAKE_DL_LIBS}
${PCRE2_LIB} ${Intl_LIBRARIES} ${ATOMIC_LIBRARY}
"fish-rust")
${PCRE2_LIB} ${Intl_LIBRARIES} ${ATOMIC_LIBRARY})
target_include_directories(fishlib PRIVATE
${CURSES_INCLUDE_DIRS})

View File

@ -10,40 +10,8 @@ In short:
- Be conservative in what you need (``C++11``, few dependencies)
- Use automated tools to help you (including ``make test``, ``build_tools/style.fish`` and ``make lint``)
Contributing completions
------------------------
Completion scripts are the most common contribution to fish, and they are very welcome.
In general, we'll take all well-written completion scripts for a command that is publically available.
This means no private tools or personal scripts, and we do reserve the right to reject for other reasons.
Before you try to contribute them to fish, consider if the authors of the tool you are completing want to maintain the script instead.
Often that makes more sense, specifically because they can add new options to the script immediately once they add them,
and don't have to maintain one completion script for multiple versions. If the authors no longer wish to maintain the script,
they can of course always contact the fish maintainers to hand it over, preferably by opening a PR.
This isn't a requirement - if the authors don't want to maintain it, or you simply don't want to contact them,
you can contribute your script to fish.
Completion scripts should
1. Use as few dependencies as possible - try to use fish's builtins like ``string`` instead of ``grep`` and ``awk``,
use ``python`` to read json instead of ``jq`` (because it's already a soft dependency for fish's tools)
2. If it uses a common unix tool, use posix-compatible invocations - ideally it would work on GNU/Linux, macOS, the BSDs and other systems
3. Option and argument descriptions should be kept short.
The shorter the description, the more likely it is that fish can use more columns.
4. Function names should start with ``__fish``, and functions should be kept in the completion file unless they're used elsewhere.
5. Run ``fish_indent`` on your script.
6. Try not to use minor convenience features right after they are available in fish - we do try to keep completion scripts backportable.
If something has a real impact on the correctness or performance, feel free to use it,
but if it is just a shortcut, please leave it.
Put your completion script into share/completions/name-of-command.fish. If you have multiple commands, you need multiple files.
If you want to add tests, you probably want to add a littlecheck test. See below for details.
Contributing to fish's C++ core
-------------------------------
General
-------
Fish uses C++11. Newer C++ features should not be used to make it possible to use on older systems.
@ -53,23 +21,37 @@ Don't introduce new dependencies unless absolutely necessary, and if you do,
please make it optional with graceful failure if possible.
Add any new dependencies to the README.rst under the *Running* and/or *Building* sections.
Linters
-------
This also goes for completion scripts and functions - if at all possible, they should only use
POSIX-compatible invocations of any tools, and no superfluous dependencies.
Automated analysis tools like cppcheck can point out
E.g. some completions deal with JSON data. In those it's preferable to use python to handle it,
as opposed to ``jq``, because fish already optionally uses python elsewhere. (It also happens to be quite a bit *faster*)
Lint Free Code
--------------
Automated analysis tools like cppcheck and oclint can point out
potential bugs or code that is extremely hard to understand. They also
help ensure the code has a consistent style and that it avoids patterns
that tend to confuse people.
To make linting the code easy there are two make targets: ``lint``,
to lint any modified but not committed ``*.cpp`` files, and
``lint-all`` to lint all files.
To make linting the code easy there are two make targets: ``lint`` and
``lint-all``. The latter does exactly what the name implies. The former
will lint any modified but not committed ``*.cpp`` files. If there is no
uncommitted work it will lint the files in the most recent commit.
Fish has custom cppcheck rules in the file ``.cppcheck.rule``. These
help catch mistakes such as using ``wcwidth()`` rather than
``fish_wcwidth()``. Please add a new rule if you find similar mistakes
being made.
Dealing With Lint Warnings
~~~~~~~~~~~~~~~~~~~~~~~~~~
You are strongly encouraged to address a lint warning by refactoring the
code, changing variable names, or whatever action is implied by the
warning.
Suppressing Lint Warnings
~~~~~~~~~~~~~~~~~~~~~~~~~
@ -91,20 +73,24 @@ following:
[src/complete.cpp:1727]: warning (nullPointerRedundantCheck): Either the condition 'cmd_node' is redundant or there is possible null pointer dereference: cmd_node.
Code Style
----------
Suppressing oclint warnings is more complicated to describe so Ill
refer you to the `OCLint
HowTo <http://docs.oclint.org/en/latest/howto/suppress.html#annotations>`__
on the topic.
To ensure your changes conform to the style rules run
Ensuring Your Changes Conform to the Style Guides
-------------------------------------------------
The following sections discuss the specific rules for the style that
should be used when writing fish code. To ensure your changes conform to
the style rules you simply need to run
::
build_tools/style.fish
before committing your change. That will run our autoformatters:
- ``git-clang-format`` for c++
- ``fish_indent`` (shipped with fish) for fish script
- ``black`` for python
before committing your change. That will run ``git-clang-format`` to
rewrite only the lines youre modifying.
If youve already committed your changes thats okay since it will then
check the files in the most recent commit. This can be useful after
@ -122,6 +108,31 @@ If you want to check the style of the entire code base run
That command will refuse to restyle any files if you have uncommitted
changes.
Configuring Your Editor for Fish C++ Code
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Vim
^^^
As of Vim 7.4 it does not recognize triple-slash comments as used by
Doxygen and the OS X Xcode IDE to flag comments that explain the
following C symbol. This means the ``gq`` key binding to reformat such
comments doesnt behave as expected. You can fix that by adding the
following to your vimrc:
::
autocmd Filetype c,cpp setlocal comments^=:///
If you use Vim I recommend the `vim-clang-format
plugin <https://github.com/rhysd/vim-clang-format>`__ by
[@rhysd](https://github.com/rhysd).
Emacs
^^^^^
If you use Emacs: TBD
Configuring Your Editor for Fish Scripts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -158,6 +169,18 @@ made to run fish_indent via e.g.
(add-hook 'fish-mode-hook (lambda ()
(add-hook 'before-save-hook 'fish_indent-before-save)))
Suppressing Reformatting of C++ Code
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can tell ``clang-format`` to not reformat a block by enclosing it in
comments like this:
::
// clang-format off
code to ignore
// clang-format on
Fish Script Style Guide
-----------------------
@ -314,6 +337,8 @@ To install the lint checkers on Mac OS X using Homebrew:
::
brew tap oclint/formulae
brew install oclint
brew install cppcheck
To install the lint checkers on Debian-based Linux distributions:
@ -321,6 +346,7 @@ To install the lint checkers on Debian-based Linux distributions:
::
sudo apt-get install clang
sudo apt-get install oclint
sudo apt-get install cppcheck
Installing the Formatting Tools
@ -420,8 +446,8 @@ Include What You Use
You should not depend on symbols being visible to a ``*.cpp`` module
from ``#include`` statements inside another header file. In other words
if your module does ``#include "common.h"`` and that header does
``#include "signals.h"`` your module should not assume the sub-include is
present. It should instead directly ``#include "signals.h"`` if it needs
``#include "signal.h"`` your module should not assume the sub-include is
present. It should instead directly ``#include "signal.h"`` if it needs
any symbol from that header. That makes the actual dependencies much
clearer. It also makes it easy to modify the headers included by a
specific header file without having to worry that will break any module

View File

@ -1,7 +1,7 @@
Fish is a smart and user-friendly command line shell.
Copyright (C) 2005-2009 Axel Liljencrantz
Copyright (C) 2009-2023 fish-shell contributors
Copyright (C) 2009-2022 fish-shell contributors
fish is free software.

View File

@ -1,8 +1,4 @@
.. |Cirrus CI| image:: https://api.cirrus-ci.com/github/fish-shell/fish-shell.svg?branch=master
:target: https://cirrus-ci.com/github/fish-shell/fish-shell
:alt: Cirrus CI Build Status
`fish <https://fishshell.com/>`__ - the friendly interactive shell |Build Status| |Cirrus CI|
`fish <https://fishshell.com/>`__ - the friendly interactive shell |Build Status|
=================================================================================
fish is a smart and user-friendly command line shell for macOS, Linux,
@ -10,7 +6,8 @@ and the rest of the family. fish includes features like syntax
highlighting, autosuggest-as-you-type, and fancy tab completions that
just work, with no configuration required.
For downloads, screenshots and more, go to https://fishshell.com/.
For more on fishs design philosophy, see the `design
document <https://fishshell.com/docs/current/design.html>`__.
Quick Start
-----------
@ -148,7 +145,6 @@ Dependencies
Compiling fish requires:
- Rust (version 1.67 or later)
- a C++11 compiler (g++ 4.8 or later, or clang 3.3 or later)
- CMake (version 3.5 or later)
- a curses implementation such as ncurses (headers and libraries)

View File

@ -96,6 +96,8 @@ def esc(m):
map = {
"\n": "\\n",
"\\": "\\\\",
"'": "\\'",
'"': '\\"',
"\a": "\\a",
"\b": "\\b",
"\f": "\\f",
@ -302,11 +304,11 @@ class TestFailure(object):
)
if b:
bstr = (
"on line "
+ str(b.line.number)
+ ": {BLUE}"
"'{BLUE}"
+ b.line.escaped_text(for_formatting=True)
+ "{RESET}"
+ "{RESET}'"
+ " on line "
+ str(b.line.number)
)
lastcheckline = b.line.number

View File

@ -1,22 +1,80 @@
#!/usr/bin/env bash
# Helper to notarize an .app.zip or .pkg file.
# Based on https://www.logcg.com/en/archives/3222.html
set -e
die() { echo "$*" 1>&2 ; exit 1; }
check_status() {
echo "STATUS" $1
}
test "$#" -ge 1 || die "No paths specified."
get_req_uuid() {
RESPONSE=$(</dev/stdin)
if echo "$RESPONSE" | egrep -q "RequestUUID"; then
echo "$RESPONSE" | egrep RequestUUID | awk '{print $3'}
elif echo "$RESPONSE" | egrep -q "The upload ID is "; then
echo "$RESPONSE" | egrep -p "The upload ID is [-a-z0-9]+" | awk '{print $5}'
else
die "Could not get Request UUID"
fi
}
for INPUT in "$@"; do
echo "Processing $INPUT"
test -f "$INPUT" || die "Not a file: $INPUT"
ext="${INPUT##*.}"
(test "$ext" = "zip" || test "$ext" = "pkg") || die "Unrecognized extension: $ext"
INPUT=$1
AC_USER=$2
xcrun notarytool submit "$INPUT" --keychain-profile AC_PASSWORD --wait
test -z "$AC_USER" && die "AC_USER not specified as second param"
test -z "$INPUT" && die "No path specified"
test -f "$INPUT" || die "Not a file: $INPUT"
ext="${INPUT##*.}"
(test "$ext" = "zip" || test "$ext" = "pkg") || die "Unrecognized extension: $ext"
LOGFILE=$(mktemp -t mac_notarize_log)
AC_PASS="@keychain:AC_PASSWORD"
echo "Logs at $LOGFILE"
NOTARIZE_UUID=$(xcrun altool --notarize-app \
--primary-bundle-id "com.ridiculousfish.fish-shell" \
--username "$AC_USER" \
--password "$AC_PASS" \
--file "$INPUT" 2>&1 |
tee -a "$LOGFILE" |
get_req_uuid)
test -z "$NOTARIZE_UUID" && cat "$LOGFILE" && die "Could not get RequestUUID"
echo "RequestUUID: $NOTARIZE_UUID"
# notarization-info doesn't always know about our request immediately.
echo "Giving notarization-info a chance to catch up..."
sleep 15
success=0
for i in $(seq 20); do
echo "Checking progress..."
PROGRESS=$(xcrun altool --notarization-info "${NOTARIZE_UUID}" \
-u "$AC_USER" \
-p "$AC_PASS" 2>&1 |
tee -a "$LOGFILE")
echo "${PROGRESS}" | tail -n 1
if [ $? -ne 0 ] || [[ "${PROGRESS}" =~ "Invalid" ]] ; then
echo "Error with notarization. Exiting"
break
fi
if ! [[ "${PROGRESS}" =~ "in progress" ]]; then
success=1
break
else
echo "Not completed yet. Sleeping for 30 seconds."
fi
sleep 30
done
if [ $success -eq 1 ] ; then
if test "$ext" = "zip"; then
TMPDIR=$(mktemp -d)
echo "Extracting to $TMPDIR"
@ -37,9 +95,9 @@ for INPUT in "$@"; do
cd "$(dirname "$STAPLE_TARGET")"
zip -r -q "$INPUT_FULL" $(basename "$STAPLE_TARGET")
fi
echo "Processed $INPUT"
fi
echo "Processed $INPUT"
if test "$ext" = "zip"; then
spctl -a -v "$STAPLE_TARGET"
fi
done
if test "$ext" = "zip"; then
spctl -a -v "$STAPLE_TARGET"
fi

View File

@ -14,14 +14,6 @@ set -e
# but to get the documentation in, we need to make a symlink called "fish-VERSION"
# and tar from that, so that the documentation gets the right prefix
# Use Ninja if available, as it automatically paralellises
BUILD_TOOL="make"
BUILD_GENERATOR="Unix Makefiles"
if command -v ninja >/dev/null; then
BUILD_TOOL="ninja"
BUILD_GENERATOR="Ninja"
fi
# We need GNU tar as that supports the --mtime and --transform options
TAR=notfound
for try in tar gtar gnutar; do
@ -59,8 +51,8 @@ git archive --format=tar --prefix="$prefix"/ HEAD > "$path"
PREFIX_TMPDIR=$(mktemp -d)
cd "$PREFIX_TMPDIR"
echo "$VERSION" > version
cmake -G "$BUILD_GENERATOR" "$wd"
$BUILD_TOOL doc
cmake "$wd"
make doc
TAR_APPEND="$TAR --append --file=$path --mtime=now --owner=0 --group=0 \
--mode=g+w,a+rX --transform s/^/$prefix\//"

View File

@ -341,25 +341,3 @@ class SpawnedProc(object):
"LIGHTCYAN": ansic(96),
"WHITE": ansic(97),
}
def control(char: str) -> str:
""" Returns the char sent when control is pressed along the given key. """
assert len(char) == 1
char = char.lower()
if ord("a") <= ord(char) <= ord("z"):
return chr(ord(char) - ord("a") + 1)
return chr({
"@": 0,
"`": 0,
"[": 27,
"{": 27,
"\\": 28,
"|": 28,
"]": 29,
"}": 29,
"^": 30,
"~": 30,
"_": 31,
"?": 127,
}[char])

View File

@ -158,6 +158,8 @@ install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/user_doc/html/ # Trailing slash is
DESTINATION ${docdir} OPTIONAL)
install(FILES CHANGELOG.rst DESTINATION ${docdir})
install(FILES share/lynx.lss DESTINATION ${rel_datadir}/fish/)
# These files are built by cmake/gettext.cmake, but using GETTEXT_PROCESS_PO_FILES's
# INSTALL_DESTINATION leads to them being installed as ${lang}.gmo, not fish.mo
# The ${languages} array comes from cmake/gettext.cmake

View File

@ -1,59 +0,0 @@
include(FetchContent)
# Don't let Corrosion's tests interfere with ours.
set(CORROSION_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/mqudsi/corrosion
GIT_TAG fish
)
FetchContent_MakeAvailable(Corrosion)
set(fish_rust_target "fish-rust")
set(fish_autocxx_gen_dir "${CMAKE_BINARY_DIR}/fish-autocxx-gen/")
if(NOT DEFINED CARGO_FLAGS)
# Corrosion doesn't like an empty string as FLAGS. This is basically a no-op alternative.
# See https://github.com/corrosion-rs/corrosion/issues/356
set(CARGO_FLAGS "--config" "foo=0")
endif()
if(DEFINED ASAN)
list(APPEND CARGO_FLAGS "-Z" "build-std")
endif()
corrosion_import_crate(
MANIFEST_PATH "${CMAKE_SOURCE_DIR}/fish-rust/Cargo.toml"
FEATURES "fish-ffi-tests"
FLAGS "${CARGO_FLAGS}"
)
# We need the build dir because cxx puts our headers in there.
# Corrosion doesn't expose the build dir, so poke where we shouldn't.
if (Rust_CARGO_TARGET)
set(rust_target_dir "${CMAKE_BINARY_DIR}/cargo/build/${_CORROSION_RUST_CARGO_TARGET}")
else()
set(rust_target_dir "${CMAKE_BINARY_DIR}/cargo/build/${_CORROSION_RUST_CARGO_HOST_TARGET}")
corrosion_set_hostbuild(${fish_rust_target})
endif()
# Tell Cargo where our build directory is so it can find config.h.
corrosion_set_env_vars(${fish_rust_target} "FISH_BUILD_DIR=${CMAKE_BINARY_DIR}" "FISH_AUTOCXX_GEN_DIR=${fish_autocxx_gen_dir}" "FISH_RUST_TARGET_DIR=${rust_target_dir}")
target_include_directories(${fish_rust_target} INTERFACE
"${rust_target_dir}/cxxbridge/${fish_rust_target}/src/"
"${fish_autocxx_gen_dir}/include/"
)
# Tell fish what extra C++ files to compile.
define_property(
TARGET PROPERTY fish_extra_cpp_files
BRIEF_DOCS "Extra C++ files to compile for fish."
FULL_DOCS "Extra C++ files to compile for fish."
)
set_property(TARGET ${fish_rust_target} PROPERTY fish_extra_cpp_files
"${fish_autocxx_gen_dir}/cxx/gen0.cxx"
)

View File

@ -175,34 +175,3 @@ foreach(PEXPECT ${PEXPECTS})
set_tests_properties(${PEXPECT} PROPERTIES ENVIRONMENT FISH_FORCE_COLOR=1)
add_test_target("${PEXPECT}")
endforeach(PEXPECT)
# Rust stuff.
if(DEFINED ASAN)
# Rust w/ -Zsanitizer=address requires explicitly specifying the --target triple or else linker
# errors pertaining to asan symbols will ensue.
if(NOT DEFINED Rust_CARGO_TARGET)
message(FATAL_ERROR "ASAN requires defining the CMake variable Rust_CARGO_TARGET to the
intended target triple")
endif()
set(cargo_target_opt "--target" ${Rust_CARGO_TARGET})
endif()
# cargo-test is failing to link w/ ASAN enabled. For some reason it is picking up autocxx ffi
# dependencies, even though `carg test` is supposed to be for rust-only code w/ no ffi dependencies.
# TODO: Figure this out and fix it.
if(NOT DEFINED ASAN)
add_test(
NAME "cargo-test"
COMMAND cargo test ${CARGO_FLAGS} --target-dir target ${cargo_target_opt}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/fish-rust"
)
set_tests_properties("cargo-test" PROPERTIES SKIP_RETURN_CODE ${SKIP_RETURN_CODE})
add_test_target("cargo-test")
endif()
add_test(
NAME "cargo-test-widestring"
COMMAND cargo test ${CARGO_FLAGS} --target-dir target ${cargo_target_opt}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/fish-rust/widestring-suffix/"
)
add_test_target("cargo-test-widestring")

4
debian/control vendored
View File

@ -6,7 +6,7 @@ Uploaders: David Adam <zanchey@ucc.gu.uwa.edu.au>
# Debhelper should be bumped to >= 10 once Ubuntu Xenial is no longer supported
Build-Depends: debhelper (>= 9.20160115), libncurses5-dev, cmake (>= 3.5.0), gettext, libpcre2-dev,
# Test dependencies
locales-all, python3, rustc (>= 1.67) | rustc-mozilla (>= 1.67)
locales-all, python3
Standards-Version: 4.1.5
Homepage: https://fishshell.com/
Vcs-Git: https://github.com/fish-shell/fish-shell.git
@ -15,7 +15,7 @@ Vcs-Browser: https://github.com/fish-shell/fish-shell
Package: fish
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, passwd (>= 4.0.3-10), gettext-base, man-db,
procps, python3 (>=3.5)
python3 (>=3.5)
Conflicts: fish-common
Recommends: xsel (>=1.2.0)
Suggests: xdg-utils

View File

@ -1,79 +0,0 @@
These is a proposed port of fish-shell from C++ to Rust, and from CMake to cargo or related. This document is high level - see the [Development Guide] for more details.
## Why Port
- Gain access to more contributors and enable easier contributions. C++ is becoming a legacy language.
- Free us from the annoyances of C++/CMake, and old toolchains.
- Ensure fish continues to be perceived as modern and relevant.
- Unlock concurrent mode (see below).
## Why Rust
- Rust is a systems programming language with broad platform support, a large community, and a relatively high probability of still being relevant in a decade.
- Rust has a unique strength in its thread safety features, which is the missing piece to enable concurrent mode - see below.
- Other languages considered:
- Java, Python and the scripting family are ruled out for startup latency and memory usage reasons.
- Go would be an awkward fit. fork is [quite the problem](https://stackoverflow.com/questions/28370646/how-do-i-fork-a-go-process/28371586#28371586) in Go.
- Other system languages (D, Nim, Zig...) are too niche: fewer contributors, higher risk of the language becoming irrelevant.
## Risks
- Large amount of work with possible introduction of new bugs.
- Long period of complicated builds.
- Existing contributors will have to learn Rust.
- As of yet unknown compatibility story for Tier 2+ platforms (Cygwin, etc).
## Approach
We will do an **incremental port** in the span of one release. We will have a period of using both C++ and Rust, and both cargo and CMake, leveraging FFI tools (see below).
The work will **proceed on master**: no long-lived branches. Tests and CI continue to pass at every commit for recent Linux and Mac. Centos7, \*BSD, etc may be temporarily disabled if they prove problematic.
The Rust code will initially resemble the replaced C++. Fidelity to existing code is more important than Rust idiomaticity, to aid review and bisecting. But don't take this to extremes - use judgement.
The port will proceed "outside in." We'll start with leaf components (e.g. builtins) and proceed towards the core. Some components will have both a Rust and C++ implementation (e.g. FLOG), in other cases we'll change the existing C++ to invoke the new Rust implementations (builtins).
After porting the C++, we'll replace CMake.
We will continue to use wide chars, locales, gettext, printf format strings, and PCRE2. We will not change the fish scripting language at all. We will _not_ use this as an opportunity to fix existing design flaws, with a few carefully chosen exceptions. See [Strings](#strings).
We will not use tokio, serde, async, or other fancy Rust frameworks initially.
### FFI
Rust/C++ interop will use [autocxx](https://github.com/google/autocxx), [Cxx](https://cxx.rs), and possibly [bindgen](https://rust-lang.github.io/rust-bindgen/). I've forked these for fish (see the [Development Guide]). Once the port is done, we will stop using them, except perhaps bindgen for PCRE2.
We will use [corrosion](https://github.com/corrosion-rs/corrosion) for CMake integration.
Inefficiencies (e.g. extra string copying) at the FFI layer are fine, since it will all get thrown away.
Tests can stay in fish_tests.cpp or be moved into Rust .rs files; either is fine.
### Strings
Rust's `String` / `&str` types cannot represent non-UTF8 filenames or data using the default encoding scheme. That's why all string conversions must go through fish's encoding scheme (using the private-use area to encode invalid sequences). For example, fish cannot use `File::open` with a `&str` because the decoding will be incorrect.
So instead of `String`, fish will use its own string type, and manage encoding and decoding as it does today. However we will make some specific changes:
1. Drop the nul-terminated requirement. When passing `const wchar_t*` back to C++, we will allocate and copy into a nul-terminated buffer.
2. Drop support for 16-bit wchar. fish will use UTF32 on all platforms, and manage conversions itself.
After the port we can consider moving to UTF-8, for memory usage reasons.
See the [Rust Development Guide][Development Guide] for more on strings.
### Thread Safety
Allowing [background functions](https://github.com/fish-shell/fish-shell/issues/238) and concurrent functions has been a goal for many years. I have been nursing [a long-lived branch](https://github.com/ridiculousfish/fish-shell/tree/concurrent_even_simpler) which allows full threaded execution. But though the changes are small, I have been reluctant to propose them, because they will make reasoning about the shell internals too complex: it is difficult in C++ to check and enforce what crosses thread boundaries.
This is Rust's bread and butter: we will encode thread requirements into our types, making it explicit and compiler-checked, via Send and Sync. Rust will allow turning on concurrent mode in a safe way, with a manageable increase in complexity, finally enabling this feature.
## Timeline
Handwaving, 6 months? Frankly unknown - there's 102 remaining .cpp files of various lengths. It'll go faster as we get better at it. Peter (ridiculous_fish) is motivated to work on this, other current contributors have some Rust as well, and we may also get new contributors from the Rust community. Part of the point is to make contribution easier.
## Links
- [Packaging Rust projects](https://wiki.archlinux.org/title/Rust_package_guidelines) from Arch Linux
[Development Guide]: rust-devel.md

View File

@ -1,173 +0,0 @@
# fish-shell Rust Development Guide
This describes how to get started building fish-shell in its partial Rust state, and how to contribute to the port.
## Overview
fish is in the process of transitioning from C++ to Rust. The fish project has a Rust crate embedded at path `fish-rust`. This crate builds a Rust library `libfish_rust.a` which is linked with the C++ `libfish.a`. Existing C++ code will be incrementally migrated to this crate; then CMake will be replaced with cargo and other Rust-native tooling.
Important tools used during this transition:
1. [Corrosion](https://github.com/corrosion-rs/corrosion) to invoke cargo from CMake.
2. [cxx](http://cxx.rs) for basic C++ <-> Rust interop.
3. [autocxx](https://google.github.io/autocxx/) for using C++ types in Rust.
We use forks of the last two - see the [FFI section](#ffi) below. No special action is required to obtain these packages. They're downloaded by cargo.
## Building
### Build Dependencies
fish-shell currently depends on Rust 1.67 or later. To install Rust, follow https://rustup.rs.
### Build via CMake
It is recommended to build inside `fish-shell/build`. This will make it easier for Rust to find the `config.h` file.
Build via CMake as normal (use any generator, here we use Ninja):
```shell
$ cd fish-shell
$ mkdir build && cd build
$ cmake -G Ninja ..
$ ninja
```
This will create the usual fish executables.
### Build just libfish_rust.a with Cargo
The directory `fish-rust` contains the Rust sources. These require that CMake has been run to produce `config.h` which is necessary for autocxx to succeed.
Follow the "Build from CMake" steps above, and then:
```shell
$ cd fish-shell/fish-rust
$ cargo build
```
This will build only the library, not a full working fish, but it allows faster iteration for Rust development. That is, after running `cmake` you can open the `fish-rust` as the root of a Rust crate, and tools like rust-analyzer will work.
## Development
The basic development loop for this port:
1. Pick a .cpp (or in some cases .h) file to port, say `util.cpp`.
2. Add the corresponding `util.rs` file to `fish-rust/`.
3. Reimplement it in Rust, along with its dependencies as needed. Match the existing C++ code where practical, including propagating any relevant comments.
- Do this even if it results in less idiomatic Rust, but avoid being super-dogmatic either way.
- One technique is to paste the C++ into the Rust code, commented out, and go line by line.
4. Decide whether any existing C++ callers should invoke the Rust implementation, or whether we should keep the C++ one.
- Utility functions may have both a Rust and C++ implementation. An example is `FLOG` where interop is too hard.
- Major components (e.g. builtin implementations) should _not_ be duplicated; instead the Rust should call C++ or vice-versa.
5. Remember to run `cargo fmt` and `cargo clippy` to keep the codebase somewhat clean (otherwise CI will fail). If you use rust-analyzer, you can run clippy automatically by setting `rust-analyzer.checkOnSave.command = "clippy"`.
You will likely run into limitations of [`autocxx`](https://google.github.io/autocxx/) and to a lesser extent [`cxx`](https://cxx.rs/). See the [FFI sections](#ffi) below.
## Type Mapping
### Constants & Type Aliases
The FFI does not support constants (`#define` or `static const`) or type aliases (`typedef`, `using`). Duplicate them using their Rust equivalent (`pub const` and `type`/`struct`/`enum`).
### Non-POD types
Many types cannot currently be passed across the language boundary by value or occur in shared structs. As a workaround, use references, raw pointers or smart pointers (`cxx` provides `SharedPtr` and `UniquePtr`). Try to keep workarounds on the C++ side and the FFI layer of the Rust code. This ensures we will get rid of the workarounds as we peel off the FFI layer.
### Strings
Fish will mostly _not_ use Rust's `String/&str` types as these cannot represent non-UTF8 data using the default encoding.
fish's primary string types will come from the [`widestring` crate](https://docs.rs/widestring). The two main string types are `WString` and `&wstr`, which are renamed [Utf32String](https://docs.rs/widestring/latest/widestring/utfstring/struct.Utf32String.html) and [Utf32Str](https://docs.rs/widestring/latest/widestring/utfstr/struct.Utf32Str.html). `WString` is an owned, heap-allocated UTF32 string, `&wstr` a borrowed UTF32 slice.
In general, follow this mapping when porting from C++:
- `wcstring` -> `WString`
- `const wcstring &` -> `&wstr`
- `const wchar_t *` -> `&wstr`
None of the Rust string types are nul-terminated. We're taking this opportunity to drop the nul-terminated aspect of wide string handling.
#### Creating strings
One may create a `&wstr` from a string literal using the `wchar::L!` macro:
```rust
use crate::wchar::{wstr, L!}
fn get_shell_name() -> &'static wstr {
L!("fish")
}
```
There is also a `widestrs` proc-macro which enables L as a _suffix_, to reduce the noise. This can be applied to any block, including modules and individual functions:
```rust
use crate::wchar::{wstr, widestrs}
#[widestrs]
fn get_shell_name() -> &'static wstr {
"fish"L // equivalent to L!("fish")
}
```
### Strings for FFI
`WString` and `&wstr` are the common strings used by Rust components. At the FII boundary there are some additional strings for interop. _All of these are temporary for the duration of the port._
- `CxxWString` is the Rust binding of `std::wstring`. It is the wide-string analog to [`CxxString`](https://cxx.rs/binding/cxxstring.html) and is [added in our fork of cxx](https://github.com/ridiculousfish/cxx/blob/fish/src/cxx_wstring.rs). This is useful for functions which return e.g. `const wcstring &`.
- `W0String` is renamed [U32CString](https://docs.rs/widestring/latest/widestring/ucstring/struct.U32CString.html). This is basically `WString` except it _is_ nul-terminated. This is useful for getting a nul-terminated `const wchar_t *` to pass to C++ implementations.
- `wcharz_t` is an annoying C++ struct which merely wraps a `const wchar_t *`, used for passing these pointers from C++ to Rust. We would prefer to use `const wchar_t *` directly but `autocxx` refuses to generate bindings for types such as `std::vector<const wchar_t *>` so we wrap it in this silly struct.
Note C++ `wchar_t`, Rust `char`, and `u32` are effectively interchangeable: you can cast pointers to them back and forth (except we check upon u32->char conversion). However be aware of which types are nul-terminated.
These types should be confined to the FFI modules, in particular `wchar_ffi`. They should not "leak" into other modules. See the `wchar_ffi` module.
### Format strings
Rust's builtin `std::fmt` modules do not accept runtime-provided format strings, so we mostly won't use them, except perhaps for FLOG / other non-translated text.
Instead we'll continue to use printf-style strings, with a Rust printf implementation.
### Vectors
See [`Vec`](https://cxx.rs/binding/vec.html) and [`CxxVector`](https://cxx.rs/binding/cxxvector.html).
In many cases, `autocxx` refuses to allow vectors of certain types. For example, autocxx supports `std::vector` and `std::shared_ptr` but NOT `std::vector<std::shared_ptr<...>>`. To work around this one can create a helper (pointer, length) struct. Example:
```cpp
struct RustFFIJobList {
std::shared_ptr<job_t> *jobs;
size_t count;
};
```
This is just a POD (plain old data) so autocxx can generate bindings for it. Then it is trivial to convert it to a Rust slice:
```
pub fn get_jobs(ffi_jobs: &ffi::RustFFIJobList) -> &[SharedPtr<job_t>] {
unsafe { slice::from_raw_parts(ffi_jobs.jobs, ffi_jobs.count) }
}
```
Another workaround is to define a struct that contains the shared pointer, and create a vector of that struct.
## Development Tooling
The [autocxx guidance](https://google.github.io/autocxx/workflow.html#how-can-i-see-what-bindings-autocxx-has-generated) is helpful:
1. Install cargo expand (`cargo install cargo-expand`). Then you can use `cargo expand` to see the generated Rust bindings for C++. In particular this is useful for seeing failed expansions for C++ types that autocxx cannot handle.
2. In rust-analyzer, enable Proc Macro and Proc Macro Attributes.
## FFI
The boundary between Rust and C++ is referred to as the Foreign Function Interface, or FFI.
`autocxx` and `cxx` both are designed for long-term interop: C++ and Rust coexisting for years. To this end, both emphasize safety: requiring lots of `unsafe`, `Pin`, etc.
fish plans to use them only temporarily, with a focus on getting things working. To this end, both cxx and autocxx have been forked to support fish:
1. Relax the requirement that all functions taking pointers are `unsafe` (this just added noise).
2. Add support for `wchar_t` as a recognized type, and `CxxWString` analogous to `CxxString`.
See the `Cargo.toml` file for the locations of the forks.

View File

@ -8,150 +8,96 @@ Synopsis
.. synopsis::
abbr --add NAME [--position command | anywhere] [-r | --regex PATTERN]
[--set-cursor[=MARKER]] ([-f | --function FUNCTION] | EXPANSION)
abbr --erase NAME ...
abbr --rename OLD_WORD NEW_WORD
abbr --add [SCOPE] WORD EXPANSION
abbr --erase WORD ...
abbr --rename [SCOPE] OLD_WORD NEW_WORD
abbr --show
abbr --list
abbr --query NAME ...
abbr --query WORD ...
Description
-----------
``abbr`` manages abbreviations - user-defined words that are replaced with longer phrases when entered.
.. note::
Only typed-in commands use abbreviations. Abbreviations are not expanded in scripts.
``abbr`` manages abbreviations - user-defined words that are replaced with longer phrases after they are entered.
For example, a frequently-run command like ``git checkout`` can be abbreviated to ``gco``.
After entering ``gco`` and pressing :kbd:`Space` or :kbd:`Enter`, the full text ``git checkout`` will appear in the command line.
To avoid expanding something that looks like an abbreviation, the default :kbd:`Control`\ +\ :kbd:`Space` binding inserts a space without expanding.
An abbreviation may match a literal word, or it may match a pattern given by a regular expression. When an abbreviation matches a word, that word is replaced by new text, called its *expansion*. This expansion may be a fixed new phrase, or it can be dynamically created via a fish function. This expansion occurs after pressing space or enter.
Options
-------
Combining these features, it is possible to create custom syntaxes, where a regular expression recognizes matching tokens, and the expansion function interprets them. See the `Examples`_ section.
The following options are available:
.. versionchanged:: 3.6.0
Previous versions of this allowed saving abbreviations in universal variables.
That's no longer possible. Existing variables will still be imported and ``abbr --erase`` will also erase the variables.
We recommend adding abbreviations to :ref:`config.fish <configuration>` by just adding the ``abbr --add`` command.
When you run ``abbr``, you will see output like this
**-a** *WORD* *EXPANSION* or **--add** *WORD* *EXPANSION*
Adds a new abbreviation, causing *WORD* to be expanded to *EXPANSION*
::
**-r** *OLD_WORD* *NEW_WORD* or **--rename** *OLD_WORD* *NEW_WORD*
Renames an abbreviation, from *OLD_WORD* to *NEW_WORD*
> abbr
abbr -a -- foo bar # imported from a universal variable, see `help abbr`
**-s** or **--show**
Show all abbreviations in a manner suitable for import and export
In that case you should take the part before the ``#`` comment and save it in :ref:`config.fish <configuration>`,
then you can run ``abbr --erase`` to remove the universal variable::
**-l** or **--list**
Lists all abbreviated words
> abbr >> ~/.config/fish/config.fish
> abbr --erase (abbr --list)
**-e** *WORD* or **--erase** *WORD* ...
Erase the given abbreviations
"add" subcommand
--------------------
**-q** or **--query**
Return 0 (true) if one of the *WORD* is an abbreviation.
.. synopsis::
**-h** or **--help**
Displays help about using this command.
abbr [-a | --add] NAME [--position command | anywhere] [-r | --regex PATTERN]
[--set-cursor[=MARKER]] ([-f | --function FUNCTION] | EXPANSION)
In addition, when adding or renaming abbreviations, one of the following **SCOPE** options can be used:
``abbr --add`` creates a new abbreviation. With no other options, the string **NAME** is replaced by **EXPANSION**.
**-g** or **--global**
Use a global variable
With **--position command**, the abbreviation will only expand when it is positioned as a command, not as an argument to another command. With **--position anywhere** the abbreviation may expand anywhere in the command line. The default is **command**.
With **--regex**, the abbreviation matches using the regular expression given by **PATTERN**, instead of the literal **NAME**. The pattern is interpreted using PCRE2 syntax and must match the entire token. If multiple abbreviations match the same token, the last abbreviation added is used.
With **--set-cursor=MARKER**, the cursor is moved to the first occurrence of **MARKER** in the expansion. The **MARKER** value is erased. The **MARKER** may be omitted (i.e. simply ``--set-cursor``), in which case it defaults to ``%``.
With **-f FUNCTION** or **--function FUNCTION**, **FUNCTION** is treated as the name of a fish function instead of a literal replacement. When the abbreviation matches, the function will be called with the matching token as an argument. If the function's exit status is 0 (success), the token will be replaced by the function's output; otherwise the token will be left unchanged. No **EXPANSION** may be given separately.
**-U** or **--universal**
Use a universal variable (default)
See the "Internals" section for more on them.
Examples
########
--------
::
abbr --add gco git checkout
abbr -a -g gco git checkout
Add a new abbreviation where ``gco`` will be replaced with ``git checkout``.
Add a new abbreviation where ``gco`` will be replaced with ``git checkout`` global to the current shell.
This abbreviation will not be automatically visible to other shells unless the same command is run in those shells (such as when executing the commands in config.fish).
::
abbr -a --position anywhere -- -C --color
abbr -a -U l less
Add a new abbreviation where ``-C`` will be replaced with ``--color``. The ``--`` allows ``-C`` to be treated as the name of the abbreviation, instead of an option.
Add a new abbreviation where ``l`` will be replaced with ``less`` universal to all shells.
Note that you omit the **-U** since it is the default.
::
abbr -a L --position anywhere --set-cursor "% | less"
Add a new abbreviation where ``L`` will be replaced with ``| less``, placing the cursor before the pipe.
abbr -r gco gch
Renames an existing abbreviation from ``gco`` to ``gch``.
::
function last_history_item
echo $history[1]
end
abbr -a !! --position anywhere --function last_history_item
abbr -e gco
This first creates a function ``last_history_item`` which outputs the last entered command. It then adds an abbreviation which replaces ``!!`` with the result of calling this function. Taken together, this is similar to the ``!!`` history expansion feature of bash.
Erase the ``gco`` abbreviation.
::
function vim_edit
echo vim $argv
end
abbr -a vim_edit_texts --position command --regex ".+\.txt" --function vim_edit
ssh another_host abbr -s | source
This first creates a function ``vim_edit`` which prepends ``vim`` before its argument. It then adds an abbreviation which matches commands ending in ``.txt``, and replaces the command with the result of calling this function. This allows text files to be "executed" as a command to open them in vim, similar to the "suffix alias" feature in zsh.
Import the abbreviations defined on another_host over SSH.
::
abbr 4DIRS --set-cursor=! "$(string join \n -- 'for dir in */' 'cd $dir' '!' 'cd ..' 'end')"
This creates an abbreviation "4DIRS" which expands to a multi-line loop "template." The template enters each directory and then leaves it. The cursor is positioned ready to enter the command to run in each directory, at the location of the ``!``, which is itself erased.
Other subcommands
--------------------
::
abbr --rename OLD_NAME NEW_NAME
Renames an abbreviation, from *OLD_NAME* to *NEW_NAME*
::
abbr [-s | --show]
Show all abbreviations in a manner suitable for import and export
::
abbr [-l | --list]
Prints the names of all abbreviation
::
abbr [-e | --erase] NAME
Erases the abbreviation with the given name
::
abbr -q or --query [NAME...]
Return 0 (true) if one of the *NAME* is an abbreviation.
::
abbr -h or --help
Displays help for the `abbr` command.
Internals
---------
Each abbreviation is stored in its own global or universal variable.
The name consists of the prefix ``_fish_abbr_`` followed by the WORD after being transformed by ``string escape style=var``.
The WORD cannot contain a space but all other characters are legal.
Abbreviations created with the **--universal** flag will be visible to other fish sessions, whilst **--global** will be limited to the current session.

View File

@ -18,8 +18,6 @@ Description
``alias`` is a simple wrapper for the ``function`` builtin, which creates a function wrapping a command. It has similar syntax to POSIX shell ``alias``. For other uses, it is recommended to define a :doc:`function <function>`.
If you want to ease your interactive use, to save typing, consider using an :doc:`abbreviation <abbr>` instead.
``fish`` marks functions that have been created by ``alias`` by including the command used to create them in the function description. You can list ``alias``-created functions by running ``alias`` without arguments. They must be erased using ``functions -e``.
- ``NAME`` is the name of the alias
@ -59,4 +57,4 @@ See more
1. The :doc:`function <function>` command this builds on.
2. :ref:`Functions <syntax-function>`.
3. :ref:`Defining aliases <syntax-aliases>`.
3. :ref:`Function wrappers <syntax-function-wrappers>`.

View File

@ -163,17 +163,6 @@ The script should write any error messages to stdout, not stderr. It should retu
Fish ships with a ``_validate_int`` function that accepts a ``--min`` and ``--max`` flag. Let's say your command accepts a ``-m`` or ``--max`` flag and the minimum allowable value is zero and the maximum is 5. You would define the option like this: ``m/max=!_validate_int --min 0 --max 5``. The default if you just call ``_validate_int`` without those flags is to simply check that the value is a valid integer with no limits on the min or max value allowed.
Here are some examples of flag validations::
# validate that a path is a directory
argparse 'p/path=!test -d "$_flag_value"' -- --path $__fish_config_dir
# validate that a function does not exist
argparse 'f/func=!not functions -q "$_flag_value"' -- -f alias
# validate that a string matches a regex
argparse 'c/color=!string match -rq \'^#?[0-9a-fA-F]{6}$\' "$_flag_value"' -- -c 'c0ffee'
# validate with a validator function
argparse 'n/num=!_validate_int --min 0 --max 99' -- --num 42
Example OPTION_SPECs
--------------------

View File

@ -33,9 +33,9 @@ To find out what sequence a key combination sends, you can use :doc:`fish_key_re
When ``COMMAND`` is a shellscript command, it is a good practice to put the actual code into a :ref:`function <syntax-function>` and simply bind to the function name. This way it becomes significantly easier to test the function while editing, and the result is usually more readable as well.
If a script produces output, it should finish by calling ``commandline -f repaint`` to tell fish that a repaint is in order.
.. note::
Special input functions cannot be combined with ordinary shell script commands. The commands must be entirely a sequence of special input functions (from ``bind -f``) or all shell script commands (i.e., valid fish script). To run special input functions from regular fish script, use ``commandline -f`` (see also :doc:`commandline <commandline>`). If a script produces output, it should finish by calling ``commandline -f repaint`` to tell fish that a repaint is in order.
Note that special input functions cannot be combined with ordinary shell script commands. The commands must be entirely a sequence of special input functions (from ``bind -f``) or all shell script commands (i.e., valid fish script).
If no ``SEQUENCE`` is provided, all bindings (or just the bindings in the given ``MODE``) are printed. If ``SEQUENCE`` is provided but no ``COMMAND``, just the binding matching that sequence is printed.

View File

@ -19,7 +19,7 @@ You can also press :kbd:`Tab` to use the completion pager to select an item from
If you give it a single argument it is equivalent to ``cd DIRECTORY``.
Note that the ``cd`` command limits directory history to the 25 most recently visited directories.
The history is stored in the :envvar:`dirprev` and :envvar:`dirnext` variables, which this command manipulates.
The history is stored in the :envvar:`dirprev` and :envvar:`$dirnext` variables, which this command manipulates.
If you make those universal variables, your ``cd`` history is shared among all fish instances.
See Also

View File

@ -34,10 +34,10 @@ The following options are available:
Causes the specified functions to be erased. This also means that it is prevented from autoloading in the current session. Use :doc:`funcsave <funcsave>` to remove the saved copy.
**-D** or **--details**
Reports the path name where the specified function is defined or could be autoloaded, ``stdin`` if the function was defined interactively or on the command line or by reading standard input, **-** if the function was created via :doc:`source <source>`, and ``n/a`` if the function isn't available. (Functions created via :doc:`alias <alias>` will return **-**, because ``alias`` uses ``source`` internally. Copied functions will return where the function was copied.) If the **--verbose** option is also specified then five lines are written:
Reports the path name where the specified function is defined or could be autoloaded, ``stdin`` if the function was defined interactively or on the command line or by reading standard input, **-** if the function was created via :doc:`source <source>`, and ``n/a`` if the function isn't available. (Functions created via :doc:`alias <alias>` will return **-**, because ``alias`` uses ``source`` internally.) If the **--verbose** option is also specified then five lines are written:
- the path name as already described,
- if the function was copied, the path name to where the function was originally defined, otherwise ``autoloaded``, ``not-autoloaded`` or ``n/a``,
- the pathname as already described,
- ``autoloaded``, ``not-autoloaded`` or ``n/a``,
- the line number within the file or zero if not applicable,
- ``scope-shadowing`` if the function shadows the vars in the calling function (the normal case if it wasn't defined with **--no-scope-shadowing**), else ``no-scope-shadowing``, or ``n/a`` if the function isn't defined,
- the function description minimally escaped so it is a single line, or ``n/a`` if the function isn't defined or has no description.

View File

@ -19,9 +19,9 @@ If a *SECTION* is specified, the help for that command is shown.
The **-h** or **--help** option displays help about using this command.
If the :envvar:`BROWSER` environment variable is set, it will be used to display the documentation.
If the :envvar:`BROWSER`` environment variable is set, it will be used to display the documentation.
Otherwise, fish will search for a suitable browser.
To use a different browser than as described above, you can set ``$fish_help_browser``
To use a different browser than as described above, one can set the :envvar:`fish_help_browser` variable.
This variable may be set as a list, where the first element is the browser command and the rest are browser options.
Example

View File

@ -29,6 +29,8 @@ Example
The following code will print ``foo.txt exists`` if the file foo.txt exists and is a regular file, otherwise it will print ``bar.txt exists`` if the file bar.txt exists and is a regular file, otherwise it will print ``foo.txt and bar.txt do not exist``.
::
if test -f foo.txt
@ -42,6 +44,7 @@ The following code will print ``foo.txt exists`` if the file foo.txt exists and
The following code will print "foo.txt exists and is readable" if foo.txt is a regular file and readable
::
if test -f foo.txt
@ -49,15 +52,3 @@ The following code will print "foo.txt exists and is readable" if foo.txt is a r
echo "foo.txt exists and is readable"
end
See also
--------
``if`` is only as useful as the command used as the condition.
Fish ships a few:
- :doc:`test` can compare numbers, strings and check paths
- :doc:`string` can perform string operations including wildcard and regular expression matches
- :doc:`path` can check paths for permissions, existence or type
- :doc:`contains` can check if an element is in a list

View File

@ -17,9 +17,9 @@ Description
To change the number of characters per path component, pass ``--dir-length=`` or set :envvar:`fish_prompt_pwd_dir_length` to the number of characters. Setting it to 0 or an invalid value will disable shortening entirely. This defaults to 1.
To keep some components unshortened, pass ``--full-length-dirs=`` or set :envvar:`fish_prompt_pwd_full_dirs` to the number of components. This defaults to 1, keeping the last component.
To keep some components unshortened, pass ``--full-length-dirs=`` or set :envvar:`$fish_prompt_pwd_full_dirs` to the number of components. This defaults to 1, keeping the last component.
If any positional arguments are given, ``prompt_pwd`` shortens them instead of :envvar:`PWD`.
If any positional arguments are given, ``prompt_pwd`` shortens them instead of $PWD.
Options
-------

View File

@ -51,8 +51,8 @@ The following scope control variable scope:
**-g** or **--global**
Sets a globally-scoped variable.
Global variables are available to all functions running in the same shell.
They can be modified or erased.
Global variables don't disappear and are available to all functions running in the same shell.
They can even be modified.
These options modify how variables operate:
@ -175,7 +175,7 @@ Remove _$smurf_ from the scope::
> set -e smurf
Remove _$smurf_ from the global and universal scopes::
Remove _$smurf_ from the global and universal scoeps::
> set -e -Ug smurf

View File

@ -87,7 +87,7 @@ In particular it will:
If terminfo reports 256 color support for a terminal, 256 color support will always be enabled.
To force true-color support on or off, set :envvar:`fish_term24bit` to "1" for on and 0 for off - ``set -g fish_term24bit 1``.
To force true-color support on or off, set :envvar:`fish_term24bit`` to "1" for on and 0 for off - ``set -g fish_term24bit 1``.
To debug color palette problems, ``tput colors`` may be useful to see the number of colors in terminfo for a terminal. Fish launched as ``fish -d term_support`` will include diagnostic messages that indicate the color support mode in use.

View File

@ -59,12 +59,6 @@ Match Glob Examples
>_ string match -i 'a??B' Axxb
Axxb
>_ string match -- '-*' -h foo --version bar
# To match things that look like options, we need a `--`
# to tell string its options end there.
-h
--version
>_ echo 'ok?' | string match '*\?'
ok?
@ -97,12 +91,6 @@ Match Regex Examples
cat4
dog4
>_ string match -r -- '-.*' -h foo --version bar
# To match things that look like options, we need a `--`
# to tell string its options end there.
-h
--version
>_ string match -r '(\d\d?):(\d\d):(\d\d)' 2:34:56
2:34:56
2

View File

@ -27,7 +27,7 @@ Note that command substitutions in a case statement will be evaluated even if it
Example
-------
If the variable ``$animal`` contains the name of an animal, the following code would attempt to classify it:
If the variable :envvar:`animal` contains the name of an animal, the following code would attempt to classify it:
::

View File

@ -11,6 +11,7 @@ Synopsis
test [EXPRESSION]
[ [EXPRESSION] ]
Description
-----------
@ -20,11 +21,13 @@ Description
To see the documentation on the ``test`` command you might have,
use ``command man test``.
``test`` checks the given conditions and sets the exit status to 0 if they are true, 1 if they are false.
Tests the expression given and sets the exit status to 0 if true, and 1 if false. An expression is made up of one or more operators and their arguments.
The first form (``test``) is preferred. For compatibility with other shells, the second form is available: a matching pair of square brackets (``[ [EXPRESSION] ]``).
When using a variable as an argument with ``test`` you should almost always enclose it in double-quotes, as variables expanding to zero or more than one argument will most likely interact badly with ``test``.
This test is mostly POSIX-compatible.
When using a variable as an argument for a test operator you should almost always enclose it in double-quotes. There are only two situations it is safe to omit the quote marks. The first is when the argument is a literal string with no whitespace or other characters special to the shell (e.g., semicolon). For example, ``test -b /my/file``. The second is using a variable that expands to exactly one element including if that element is the empty string (e.g., ``set x ''``). If the variable is not set, set but with no value, or set to more than one value you must enclose it in double-quotes. For example, ``test "$x" = "$y"``. Since it is always safe to enclose variables in double-quotes when used as ``test`` arguments that is the recommended practice.
Operators for files and directories
-----------------------------------
@ -160,6 +163,8 @@ Examples
If the ``/tmp`` directory exists, copy the ``/etc/motd`` file to it:
::
if test -d /tmp
@ -169,6 +174,8 @@ If the ``/tmp`` directory exists, copy the ``/etc/motd`` file to it:
If the variable :envvar:`MANPATH` is defined and not empty, print the contents. (If :envvar:`MANPATH` is not defined, then it will expand to zero arguments, unless quoted.)
::
if test -n "$MANPATH"
@ -178,6 +185,8 @@ If the variable :envvar:`MANPATH` is defined and not empty, print the contents.
Parentheses and the ``-o`` and ``-a`` operators can be combined to produce more complicated expressions. In this example, success is printed if there is a ``/foo`` or ``/bar`` file as well as a ``/baz`` or ``/bat`` file.
::
if test \( -f /foo -o -f /bar \) -a \( -f /baz -o -f /bat \)
@ -187,22 +196,30 @@ Parentheses and the ``-o`` and ``-a`` operators can be combined to produce more
Numerical comparisons will simply fail if one of the operands is not a number:
::
if test 42 -eq "The answer to life, the universe and everything"
echo So long and thanks for all the fish # will not be executed
end
A common comparison is with :envvar:`status`:
::
if test $status -eq 0
echo "Previous command succeeded"
end
The previous test can likewise be inverted:
::
if test ! $status -eq 0
@ -212,6 +229,8 @@ The previous test can likewise be inverted:
which is logically equivalent to the following:
::
if test $status -ne 0
@ -222,16 +241,10 @@ which is logically equivalent to the following:
Standards
---------
Unlike many things in fish, ``test`` implements a subset of the `IEEE Std 1003.1-2008 (POSIX.1) standard <https://www.unix.com/man-page/posix/1p/test/>`__. The following exceptions apply:
``test`` implements a subset of the `IEEE Std 1003.1-2008 (POSIX.1) standard <https://www.unix.com/man-page/posix/1p/test/>`__. The following exceptions apply:
- The ``<`` and ``>`` operators for comparing strings are not implemented.
- Because this test is a shell builtin and not a standalone utility, using the -c flag on a special file descriptors like standard input and output may not return the same result when invoked from within a pipe as one would expect when invoking the ``test`` utility in another shell.
In cases such as this, one can use ``command`` ``test`` to explicitly use the system's standalone ``test`` rather than this ``builtin`` ``test``.
See also
--------
Other commands that may be useful as a condition, and are often easier to use:
- :doc:`string`, which can do string operations including wildcard and regular expression matching
- :doc:`path`, which can do file checks and operations, including filters on multiple paths at once

View File

@ -25,31 +25,6 @@ For checking timing after a command has completed, check :ref:`$CMD_DURATION <va
Your system most likely also has a ``time`` command. To use that use something like ``command time``, as in ``command time sleep 10``. Because it's not inside fish, it won't have access to fish functions and won't be able to time blocks and such.
How to interpret the output
---------------------------
Time outputs a few different values. Let's look at an example::
> time string repeat -n 10000000 y\n | command grep y >/dev/null
________________________________________________________
Executed in 805.98 millis fish external
usr time 798.88 millis 763.88 millis 34.99 millis
sys time 141.22 millis 40.20 millis 101.02 millis
The time after "Executed in" is what is known as the "wall-clock time". It is simply a measure of how long it took from the start of the command until it finished. Typically it is reasonably close to :envvar:`CMD_DURATION`, except for a slight skew because the two are taken at slightly different times.
The other times are all measures of CPU time. That means they measure how long the CPU was used in this part, and they count multiple cores separately. So a program with four threads using all CPU for a second will have a time of 4 seconds.
The "usr" time is how much CPU time was spent inside the program itself, the "sys" time is how long was spent in the kernel on behalf of that program.
The "fish" time is how much CPU was spent in fish, the "external" time how much was spent in external commands.
So in this example, since ``string`` is a builtin, everything that ``string repeat`` did is accounted to fish. Any time it spends doing syscalls like ``write()`` is accounted for in the fish/sys time.
And ``grep`` here is explicitly invoked as an external command, so its times will be counted in the "external" column.
Note that, as in this example, the CPU times can add up to more than the execution time. This is because things can be done in parallel - ``grep`` can match while ``string repeat`` writes.
Example
-------

View File

@ -20,18 +20,19 @@ Core language keywords that make up the syntax, like
- :doc:`begin <cmds/begin>` to begin a block and :doc:`end <cmds/end>` to end any block (including ifs and loops).
- :doc:`and <cmds/and>`, :doc:`or <cmds/or>` and :doc:`not <cmds/not>` to combine commands logically.
- :doc:`switch <cmds/switch>` and :doc:`case <cmds/case>` to make multiple blocks depending on the value of a variable.
- :doc:`command <cmds/command>` or :doc:`builtin <cmds/builtin>` to tell fish what sort of thing to execute
- :doc:`time <cmds/time>` to time execution
- :doc:`exec <cmds/exec>` tells fish to replace itself with a command.
Tools
^^^^^
Decorations
^^^^^^^^^^^
Command decorations are keywords like :doc:`command <cmds/command>` or :doc:`builtin <cmds/builtin>` to tell fish what sort of thing to execute, and :doc:`time <cmds/time>` to time its execution. :doc:`exec <cmds/exec>` tells fish to replace itself with the command.
Tools to do a task
^^^^^^^^^^^^^^^^^^
Builtins to do a task, like
- :doc:`cd <cmds/cd>` to change the current directory.
- :doc:`echo <cmds/echo>` or :doc:`printf <cmds/printf>` to produce output.
- :doc:`set_color <cmds/set_color>` to colorize output.
- :doc:`set <cmds/set>` to set, query or erase variables.
- :doc:`read <cmds/read>` to read input.
- :doc:`string <cmds/string>` for string manipulation.
@ -41,11 +42,11 @@ Builtins to do a task, like
- :doc:`type <cmds/type>` to find out what sort of thing (command, builtin or function) fish would call, or if it exists at all.
- :doc:`test <cmds/test>` checks conditions like if a file exists or a string is empty.
- :doc:`contains <cmds/contains>` to see if a list contains an entry.
- :doc:`eval <cmds/eval>` and :doc:`source <cmds/source>` to run fish code from a string or file.
- :doc:`status <cmds/status>` to get shell information, like whether it's interactive or a login shell, or which file it is currently running.
- :doc:`abbr <cmds/abbr>` manages :ref:`abbreviations`.
- :doc:`eval <cmds/eval>` and :doc:`source <cmds/source>` to run fish code from a string or file.
- :doc:`set_color <cmds/set_color>` to colorize your output.
- :doc:`status <cmds/status>` to get shell information, like whether it's interactive or a login shell, or which file it is currently running.
- :doc:`bind <cmds/bind>` to change bindings.
- :doc:`complete <cmds/complete>` manages :ref:`completions <tab-completion>`.
- :doc:`commandline <cmds/commandline>` to get or change the commandline contents.
- :doc:`fish_config <cmds/fish_config>` to easily change fish's configuration, like the prompt or colorscheme.
- :doc:`random <cmds/random>` to generate random numbers or pick from a list.
@ -75,7 +76,6 @@ Some helper functions, often to give you information for use in your prompt:
- :doc:`fish_is_root_user <cmds/fish_is_root_user>` to check if the current user is an administrator user like root.
- :doc:`fish_add_path <cmds/fish_add_path>` to easily add a path to $PATH.
- :doc:`alias <cmds/alias>` to quickly define wrapper functions ("aliases").
- :doc:`fish_delta <cmds/fish_delta>` to show what you have changed from the default configuration.
Helper commands
^^^^^^^^^^^^^^^

View File

@ -59,7 +59,7 @@ highlight_language = "fish-docs-samples"
# -- Project information -----------------------------------------------------
project = "fish-shell"
copyright = "2023, fish-shell developers"
copyright = "2022, fish-shell developers"
author = "fish-shell developers"
issue_url = "https://github.com/fish-shell/fish-shell/issues"

View File

@ -25,7 +25,7 @@ Variables
Fish sets and erases variables with :doc:`set <cmds/set>` instead of ``VAR=VAL`` and a variety of separate builtins like ``declare`` and ``unset`` and ``export``. ``set`` takes options to determine the scope and exportedness of a variable::
# Define $PAGER *g*lobal and e*x*ported,
# Define $PAGER global and exported,
# so this is like ``export PAGER=less``
set -gx PAGER less
@ -60,8 +60,7 @@ And here is fish::
> set foo "bar baz"
> printf '"%s"\n' $foo
# foo was set as one element,
# so it will be passed as one element, so this is one line
# foo was set as one element, so it will be passed as one element, so this is one line
"bar baz"
All variables are "arrays" (we use the term "lists"), and expanding a variable expands to all its elements, with each element as its own argument (like bash's ``"${var[@]}"``::

View File

@ -100,19 +100,10 @@ A script written in :command:`bash` would need a first line like this:
When the shell tells the kernel to execute the file, it will use the interpreter ``/bin/bash``.
For a script written in another language, just replace ``/bin/bash`` with the interpreter for that language. For example: ``/usr/bin/python`` for a python script, or ``/usr/local/bin/fish`` for a fish script, if that is where you have them installed.
For a script written in another language, just replace ``/bin/bash`` with the interpreter for that language (for example: ``/usr/bin/python`` for a python script, or ``/usr/local/bin/fish`` for a fish script).
If you want to share your script with others, you might want to use :command:`env` to allow for the interpreter to be installed in other locations. For example::
This line is only needed when scripts are executed without specifying the interpreter. For functions inside fish or when executing a script with ``fish /path/to/script``, a shebang is not required (but it doesn't hurt!).
#!/usr/bin/env fish
echo Hello from fish $version
This will call ``env``, which then goes through :envvar:`PATH` to find a program called "fish". This makes it work, whether fish is installed in (for example) ``/usr/local/bin/fish``, ``/usr/bin/fish``, or ``~/.local/bin/fish``, as long as that directory is in :envvar:`PATH`.
The shebang line is only used when scripts are executed without specifying the interpreter. For functions inside fish or when executing a script with ``fish /path/to/script``, a shebang is not required (but it doesn't hurt!).
When executing files without an interpreter, fish, like other shells, tries your system shell, typically ``/bin/sh``. This is needed because some scripts are shipped without a shebang line.
Configuration
=============

View File

@ -42,22 +42,32 @@ Tab completion is a time saving feature of any modern shell. When you type :kbd:
The pager can be navigated with the arrow keys, :kbd:`Page Up` / :kbd:`Page Down`, :kbd:`Tab` or :kbd:`Shift`\ +\ :kbd:`Tab`. Pressing :kbd:`Control`\ +\ :kbd:`S` (the ``pager-toggle-search`` binding - :kbd:`/` in vi-mode) opens up a search menu that you can use to filter the list.
Fish provides some general purpose completions, like for commands, variable names, usernames or files.
Fish provides some general purpose completions:
It also provides a large number of program specific scripted completions. Most of these completions are simple options like the ``-l`` option for ``ls``, but a lot are more advanced. For example:
- Commands (builtins, functions and regular programs).
- ``man`` and ``whatis`` show the installed manual pages as completions.
- Shell variable names.
- ``make`` uses targets in the Makefile in the current directory as completions.
- Usernames for tilde expansion.
- ``mount`` uses mount points specified in fstab as completions.
- Filenames, even on strings with wildcards such as ``*`` and ``**``.
- ``apt``, ``rpm`` and ``yum`` show installed or installable packages
It also provides a large number of program specific scripted completions. Most of these completions are simple options like the ``-l`` option for ``ls``, but some are more advanced. For example:
- The programs ``man`` and ``whatis`` show all installed manual pages as completions.
- The ``make`` program uses all targets in the Makefile in the current directory as completions.
- The ``mount`` command uses all mount points specified in fstab as completions.
- The ``ssh`` command uses all hosts that are stored in the known_hosts file as completions. (See the ssh documentation for more information)
- The ``su`` command shows the users on the system
- The ``apt-get``, ``rpm`` and ``yum`` commands show installed or installable packages
You can also write your own completions or install some you got from someone else. For that, see :ref:`Writing your own completions <completion-own>`.
Completion scripts are loaded on demand, just like :ref:`functions are <syntax-function-autoloading>`. The difference is the ``$fish_complete_path`` :ref:`list <variables-lists>` is used instead of ``$fish_function_path``. Typically you can drop new completions in ~/.config/fish/completions/name-of-command.fish and fish will find them automatically.
.. _color:
Syntax highlighting
@ -99,34 +109,34 @@ Example: to make errors highlighted and red, use::
The following variables are available to change the highlighting colors in fish:
========================================== =====================================================================
Variable Meaning
========================================== =====================================================================
.. envvar:: fish_color_normal default color
.. envvar:: fish_color_command commands like echo
.. envvar:: fish_color_keyword keywords like if - this falls back on the command color if unset
.. envvar:: fish_color_quote quoted text like ``"abc"``
.. envvar:: fish_color_redirection IO redirections like >/dev/null
.. envvar:: fish_color_end process separators like ``;`` and ``&``
.. envvar:: fish_color_error syntax errors
.. envvar:: fish_color_param ordinary command parameters
.. envvar:: fish_color_valid_path parameters that are filenames (if the file exists)
.. envvar:: fish_color_option options starting with "-", up to the first "--" parameter
.. envvar:: fish_color_comment comments like '# important'
.. envvar:: fish_color_selection selected text in vi visual mode
.. envvar:: fish_color_operator parameter expansion operators like ``*`` and ``~``
.. envvar:: fish_color_escape character escapes like ``\n`` and ``\x70``
.. envvar:: fish_color_autosuggestion autosuggestions (the proposed rest of a command)
.. envvar:: fish_color_cwd the current working directory in the default prompt
.. envvar:: fish_color_cwd_root the current working directory in the default prompt for the root user
.. envvar:: fish_color_user the username in the default prompt
.. envvar:: fish_color_host the hostname in the default prompt
.. envvar:: fish_color_host_remote the hostname in the default prompt for remote sessions (like ssh)
.. envvar:: fish_color_status the last command's nonzero exit code in the default prompt
.. envvar:: fish_color_cancel the '^C' indicator on a canceled command
.. envvar:: fish_color_search_match history search matches and selected pager items (background only)
========================================== =====================================================================
Variable Meaning
========================================== =====================================================================
``fish_color_normal`` default color
``fish_color_command`` commands like echo
``fish_color_keyword`` keywords like if - this falls back on the command color if unset
``fish_color_quote`` quoted text like ``"abc"``
``fish_color_redirection`` IO redirections like >/dev/null
``fish_color_end`` process separators like ``;`` and ``&``
``fish_color_error`` syntax errors
``fish_color_param`` ordinary command parameters
``fish_color_valid_path`` parameters that are filenames (if the file exists)
``fish_color_option`` options starting with "-", up to the first "--" parameter
``fish_color_comment`` comments like '# important'
``fish_color_selection`` selected text in vi visual mode
``fish_color_operator`` parameter expansion operators like ``*`` and ``~``
``fish_color_escape`` character escapes like ``\n`` and ``\x70``
``fish_color_autosuggestion`` autosuggestions (the proposed rest of a command)
``fish_color_cwd`` the current working directory in the default prompt
``fish_color_cwd_root`` the current working directory in the default prompt for the root user
``fish_color_user`` the username in the default prompt
``fish_color_host`` the hostname in the default prompt
``fish_color_host_remote`` the hostname in the default prompt for remote sessions (like ssh)
``fish_color_status`` the last command's nonzero exit code in the default prompt
``fish_color_cancel`` the '^C' indicator on a canceled command
``fish_color_search_match`` history search matches and selected pager items (background only)
========================================== =====================================================================
========================================== =====================================================================
If a variable isn't set or is empty, fish usually tries ``$fish_color_normal``, except for:
@ -156,23 +166,23 @@ To have black text on alternating white and gray backgrounds::
Variables affecting the pager colors:
=================================================== ===========================================================
========================================== ===========================================================
Variable Meaning
=================================================== ===========================================================
.. envvar:: fish_pager_color_progress the progress bar at the bottom left corner
.. envvar:: fish_pager_color_background the background color of a line
.. envvar:: fish_pager_color_prefix the prefix string, i.e. the string that is to be completed
.. envvar:: fish_pager_color_completion the completion itself, i.e. the proposed rest of the string
.. envvar:: fish_pager_color_description the completion description
.. envvar:: fish_pager_color_selected_background background of the selected completion
.. envvar:: fish_pager_color_selected_prefix prefix of the selected completion
.. envvar:: fish_pager_color_selected_completion suffix of the selected completion
.. envvar:: fish_pager_color_selected_description description of the selected completion
.. envvar:: fish_pager_color_secondary_background background of every second unselected completion
.. envvar:: fish_pager_color_secondary_prefix prefix of every second unselected completion
.. envvar:: fish_pager_color_secondary_completion suffix of every second unselected completion
.. envvar:: fish_pager_color_secondary_description description of every second unselected completion
=================================================== ===========================================================
========================================== ===========================================================
``fish_pager_color_progress`` the progress bar at the bottom left corner
``fish_pager_color_background`` the background color of a line
``fish_pager_color_prefix`` the prefix string, i.e. the string that is to be completed
``fish_pager_color_completion`` the completion itself, i.e. the proposed rest of the string
``fish_pager_color_description`` the completion description
``fish_pager_color_selected_background`` background of the selected completion
``fish_pager_color_selected_prefix`` prefix of the selected completion
``fish_pager_color_selected_completion`` suffix of the selected completion
``fish_pager_color_selected_description`` description of the selected completion
``fish_pager_color_secondary_background`` background of every second unselected completion
``fish_pager_color_secondary_prefix`` prefix of every second unselected completion
``fish_pager_color_secondary_completion`` suffix of every second unselected completion
``fish_pager_color_secondary_description`` description of every second unselected completion
========================================== ===========================================================
When the secondary or selected variables aren't set or are empty, the normal variables are used, except for ``$fish_pager_color_selected_background``, where the background of ``$fish_color_search_match`` is tried first.
@ -187,20 +197,9 @@ To avoid needless typing, a frequently-run command like ``git checkout`` can be
abbr -a gco git checkout
After entering ``gco`` and pressing :kbd:`Space` or :kbd:`Enter`, a ``gco`` in command position will turn into ``git checkout`` in the command line. If you want to use a literal ``gco`` sometimes, use :kbd:`Control`\ +\ :kbd:`Space` [#]_.
After entering ``gco`` and pressing :kbd:`Space` or :kbd:`Enter`, the full text ``git checkout`` will appear in the command line.
This is a lot more powerful, for example you can make going up a number of directories easier with this::
function multicd
echo cd (string repeat -n (math (string length -- $argv[1]) - 1) ../)
end
abbr --add dotdot --regex '^\.\.+$' --function multicd
Now, ``..`` transforms to ``cd ../``, while ``...`` turns into ``cd ../../`` and ``....`` expands to ``cd ../../../``.
The advantage over aliases is that you can see the actual command before using it, add to it or change it, and the actual command will be stored in history.
.. [#] Any binding that executes the ``expand-abbr`` or ``execute`` :doc:`bind function <cmds/bind>` will expand abbreviations. By default :kbd:`Control`\ +\ :kbd:`Space` is bound to just inserting a space.
This is an alternative to aliases, and has the advantage that you see the actual command before using it, and the actual command will be stored in history.
.. _title:
@ -235,21 +234,7 @@ The output of the former is displayed on the left and the latter's output on the
Configurable greeting
---------------------
When it is started interactively, fish tries to run the :doc:`fish_greeting <cmds/fish_greeting>` function. The default fish_greeting prints a simple greeting. You can change its text by changing the ``$fish_greeting`` variable, for instance using a :ref:`universal variable <variables-universal>`::
set -U fish_greeting
or you can set it :ref:`globally <variables-scope>` in :ref:`config.fish <configuration>`::
set -g fish_greeting 'Hey, stranger!'
or you can script it by changing the function::
function fish_greeting
random choice "Hello!" "Hi" "G'day" "Howdy"
end
save this in config.fish or :ref:`a function file <syntax-function-autoloading>`. You can also use :doc:`funced <cmds/funced>` and :doc:`funcsave <cmds/funcsave>` to edit it easily.
When it is started interactively, fish tries to run the :doc:`fish_greeting <cmds/fish_greeting>` function. The default fish_greeting prints a simple greeting. You can change its text by changing the ``$fish_greeting`` variable.
.. _private-mode:
@ -286,11 +271,11 @@ While the key bindings included with fish include many of the shortcuts popular
.. _shared-binds:
Shared bindings
^^^^^^^^^^^^^^^
---------------
Some bindings are common across Emacs and Vi mode, because they aren't text editing bindings, or because what Vi/Vim does for a particular key doesn't make sense for a shell.
- :kbd:`Tab` :ref:`completes <tab-completion>` the current token. :kbd:`Shift`\ +\ :kbd:`Tab` completes the current token and starts the pager's search mode. :kbd:`Tab` is the same as :kbd:`Control`\ +\ :kbd:`I`.
- :kbd:`Tab` :ref:`completes <tab-completion>` the current token. :kbd:`Shift`\ +\ :kbd:`Tab` completes the current token and starts the pager's search mode.
- :kbd:`←` (Left) and :kbd:`→` (Right) move the cursor left or right by one character. If the cursor is already at the end of the line, and an autosuggestion is available, :kbd:`→` accepts the autosuggestion.
@ -312,11 +297,11 @@ Some bindings are common across Emacs and Vi mode, because they aren't text edit
- :kbd:`Control`\ +\ :kbd:`D` delete one character to the right of the cursor. If the command line is empty, :kbd:`Control`\ +\ :kbd:`D` will exit fish.
- :kbd:`Control`\ +\ :kbd:`U` removes contents from the beginning of line to the cursor (moving it to the :ref:`killring <killring>`).
- :kbd:`Control`\ +\ :kbd:`U` moves contents from the beginning of line to the cursor to the :ref:`killring <killring>`.
- :kbd:`Control`\ +\ :kbd:`L` clears and repaints the screen.
- :kbd:`Control`\ +\ :kbd:`W` removes the previous path component (everything up to the previous "/", ":" or "@") (moving it to the :ref:`killring`).
- :kbd:`Control`\ +\ :kbd:`W` moves the previous path component (everything up to the previous "/", ":" or "@") to the :ref:`killring`.
- :kbd:`Control`\ +\ :kbd:`X` copies the current buffer to the system's clipboard, :kbd:`Control`\ +\ :kbd:`V` inserts the clipboard contents. (see :doc:`fish_clipboard_copy <cmds/fish_clipboard_copy>` and :doc:`fish_clipboard_paste <cmds/fish_clipboard_paste>`)
@ -343,7 +328,7 @@ Some bindings are common across Emacs and Vi mode, because they aren't text edit
.. _emacs-mode:
Emacs mode commands
^^^^^^^^^^^^^^^^^^^
-------------------
To enable emacs mode, use ``fish_default_key_bindings``. This is also the default.
@ -355,15 +340,9 @@ To enable emacs mode, use ``fish_default_key_bindings``. This is also the defaul
- :kbd:`Control`\ +\ :kbd:`N`, :kbd:`Control`\ +\ :kbd:`P` move the cursor up/down or through history, like the up and down arrow shared bindings.
- :kbd:`Delete` or :kbd:`Backspace` removes one character forwards or backwards respectively. This also goes for :kbd:`Control`\ +\ :kbd:`H`, which is indistinguishable from backspace.
- :kbd:`Delete` or :kbd:`Backspace` removes one character forwards or backwards respectively.
- :kbd:`Alt`\ +\ :kbd:`Backspace` removes one word backwards.
- :kbd:`Alt`\ +\ :kbd:`<` moves to the beginning of the commandline, :kbd:`Alt`\ +\ :kbd:`>` moves to the end.
- :kbd:`Control`\ +\ :kbd:`K` deletes from the cursor to the end of line (moving it to the :ref:`killring`).
- :kbd:`Escape` and :kbd:`Control`\ +\ :kbd:`G` cancel the current operation. Immediately after an unambiguous completion this undoes it.
- :kbd:`Control`\ +\ :kbd:`K` moves contents from the cursor to the end of line to the :ref:`killring`.
- :kbd:`Alt`\ +\ :kbd:`C` capitalizes the current word.
@ -386,7 +365,7 @@ You can change these key bindings using the :doc:`bind <cmds/bind>` builtin.
.. _vi-mode:
Vi mode commands
^^^^^^^^^^^^^^^^
----------------
Vi mode allows for the use of Vi-like commands at the prompt. Initially, :ref:`insert mode <vi-mode-insert>` is active. :kbd:`Escape` enters :ref:`command mode <vi-mode-command>`. The commands available in command, insert and visual mode are described below. Vi mode shares :ref:`some bindings <shared-binds>` with :ref:`Emacs mode <emacs-mode>`.
@ -420,9 +399,6 @@ The ``fish_vi_cursor`` function will be used to change the cursor's shape depend
set fish_cursor_insert line
# Set the replace mode cursor to an underscore
set fish_cursor_replace_one underscore
# Set the external cursor to a line. The external cursor appears when a command is started.
# The cursor shape takes the value of fish_cursor_default when fish_cursor_external is not specified.
set fish_cursor_external line
# The following variable can be used to configure cursor shape in
# visual mode, but due to fish_cursor_default, is redundant here
set fish_cursor_visual block
@ -434,7 +410,7 @@ If the cursor shape does not appear to be changing after setting the above varia
.. _vi-mode-command:
Command mode
""""""""""""
^^^^^^^^^^^^
Command mode is also known as normal mode.
@ -454,10 +430,6 @@ Command mode is also known as normal mode.
- :kbd:`Shift`\ +\ :kbd:`A` enters :ref:`insert mode <vi-mode-insert>` at the end of the line.
- :kbd:`o` inserts a new line under the current one and enters :ref:`insert mode <vi-mode-insert>`
- :kbd:`O` (capital-"o") inserts a new line above the current one and enters :ref:`insert mode <vi-mode-insert>`
- :kbd:`0` (zero) moves the cursor to beginning of line (remaining in command mode).
- :kbd:`d`\ +\ :kbd:`d` deletes the current line and moves it to the :ref:`killring`.
@ -467,7 +439,6 @@ Command mode is also known as normal mode.
- :kbd:`p` pastes text from the :ref:`killring`.
- :kbd:`u` undoes the most recent edit of the command line.
- :kbd:`Control`\ +\ :kbd:`R` redoes the most recent edit.
- :kbd:`[` and :kbd:`]` search the command history for the previous/next token containing the token under the cursor before the search was started. See the :ref:`history <history-search>` section for more information on history searching.
@ -475,14 +446,10 @@ Command mode is also known as normal mode.
- :kbd:`Backspace` moves the cursor left.
- :kbd:`g` / :kbd:`G` moves the cursor to the beginning/end of the commandline, respectively.
- :kbd:`:q` exits fish.
.. _vi-mode-insert:
Insert mode
"""""""""""
^^^^^^^^^^^
- :kbd:`Escape` enters :ref:`command mode <vi-mode-command>`.
@ -491,7 +458,7 @@ Insert mode
.. _vi-mode-visual:
Visual mode
"""""""""""
^^^^^^^^^^^
- :kbd:`←` (Left) and :kbd:`→` (Right) extend the selection backward/forward by one character.
@ -522,7 +489,7 @@ Visual mode
.. _custom-binds:
Custom bindings
^^^^^^^^^^^^^^^
---------------
In addition to the standard bindings listed here, you can also define your own with :doc:`bind <cmds/bind>`::
@ -531,18 +498,7 @@ In addition to the standard bindings listed here, you can also define your own w
Put ``bind`` statements into :ref:`config.fish <configuration>` or a function called ``fish_user_key_bindings``.
If you change your mind on a binding and want to go back to fish's default, you can simply erase it again::
bind --erase \cc
Fish remembers its preset bindings and so it will take effect again. This saves you from having to remember what it was before and add it again yourself.
Key sequences
"""""""""""""
The terminal tells fish which keys you pressed by sending some sequences of bytes to describe that key. For some keys, this is easy - pressing :kbd:`a` simply means the terminal sends "a". In others it's more complicated and terminals disagree on which they send.
In these cases, :doc:`fish_key_reader <cmds/fish_key_reader>` can tell you how to write the key sequence for your terminal. Just start it and press the keys you are interested in::
The key sequence (the ``\cc``) here depends on your setup, in particular the terminal. To find out what the terminal sends use :doc:`fish_key_reader <cmds/fish_key_reader>`::
> fish_key_reader # pressing control-c
Press a key:
@ -564,7 +520,7 @@ If you want to be able to press :kbd:`Escape` and then a character and have it c
.. _killring:
Copy and paste (Kill Ring)
^^^^^^^^^^^^^^^^^^^^^^^^^^
--------------------------
Fish uses an Emacs-style kill ring for copy and paste functionality. For example, use :kbd:`Control`\ +\ :kbd:`K` (`kill-line`) to cut from the current cursor position to the end of the line. The string that is cut (a.k.a. killed in emacs-ese) is inserted into a list of kills, called the kill ring. To paste the latest value from the kill ring (emacs calls this "yanking") use :kbd:`Control`\ +\ :kbd:`Y` (the ``yank`` input function). After pasting, use :kbd:`Alt`\ +\ :kbd:`Y` (``yank-pop``) to rotate to the previous kill.
@ -573,14 +529,14 @@ In addition, when pasting inside single quotes, pasted single quotes and backsla
Kill ring entries are stored in ``fish_killring`` variable.
The commands ``begin-selection`` and ``end-selection`` (unbound by default; used for selection in vi visual mode) control text selection together with cursor movement commands that extend the current selection.
The variable :envvar:`fish_cursor_selection_mode` can be used to configure if that selection should include the character under the cursor (``inclusive``) or not (``exclusive``). The default is ``exclusive``, which works well with any cursor shape. For vi mode, and particularly for the ``block`` or ``underscore`` cursor shapes you may prefer ``inclusive``.
The configuration setting ``$fish_cursor_selection_mode`` can be used to configure if that selection should include the character under the cursor (``inclusive``) or not (``exclusive``). The default is ``exclusive``, which works well with any cursor shape. For vi mode, and particularly for the ``block`` or ``underscore`` cursor shapes you may prefer ``inclusive``.
.. [#] These rely on external tools. Currently xsel, xclip, wl-copy/wl-paste and pbcopy/pbpaste are supported.
.. _multiline:
Multiline editing
^^^^^^^^^^^^^^^^^
-----------------
The fish commandline editor can be used to work on commands that are several lines long. There are three ways to make a command span more than a single line:
@ -595,7 +551,7 @@ The fish commandline editor works exactly the same in single line mode and in mu
.. _history-search:
Searchable command history
^^^^^^^^^^^^^^^^^^^^^^^^^^
--------------------------
After a command has been executed, it is remembered in the history list. Any duplicate history items are automatically removed. By pressing the up and down keys, you can search forwards and backwards in the history. If the current command line is not empty when starting a history search, only the commands containing the string entered into the command line are shown.

View File

@ -21,13 +21,15 @@ Shells like fish are used by giving them commands. A command is executed by writ
Everything in fish is done with commands. There are commands for repeating other commands, commands for assigning variables, commands for treating a group of commands as a single command, etc. All of these commands follow the same basic syntax.
To learn more about the ``echo`` command, read its manual page by typing ``man echo``. ``man`` is a command for displaying a manual page on a given topic. It takes the name of the manual page to display as an argument. There are manual pages for almost every command. There are also manual pages for many other things, such as system libraries and important files.
Every program on your computer can be used as a command in fish. If the program file is located in one of the :envvar:`PATH` directories, you can just type the name of the program to use it. Otherwise the whole filename, including the directory (like ``/home/me/code/checkers/checkers`` or ``../checkers``) is required.
Here is a list of some useful commands:
- :doc:`cd <cmds/cd>`: Change the current directory
- ``ls``: List files and directories
- ``man``: Display a manual page - try ``man ls`` to get help on your "ls" command, or ``man mv`` to get information about "mv".
- ``man``: Display a manual page
- ``mv``: Move (rename) files
- ``cp``: Copy files
- :doc:`open <cmds/open>`: Open files with the default application associated with each filetype
@ -37,9 +39,7 @@ Commands and arguments are separated by the space character ``' '``. Every comma
A switch is a very common special type of argument. Switches almost always start with one or more hyphens ``-`` and alter the way a command operates. For example, the ``ls`` command usually lists the names of all files and directories in the current working directory. By using the ``-l`` switch, the behavior of ``ls`` is changed to not only display the filename, but also the size, permissions, owner, and modification time of each file.
Switches differ between commands and are usually documented on a command's manual page. There are some switches, however, that are common to most commands. For example, ``--help`` will usually display a help text, ``--version`` will usually display the command version, and ``-i`` will often turn on interactive prompting before taking action. Try ``man your-command-here`` to get information on your command's switches.
So the basic idea of fish is the same as with other unix shells: It gets a commandline, runs :ref:`expansions <expand>`, and the result is then run as a command.
Switches differ between commands and are usually documented on a command's manual page. There are some switches, however, that are common to most commands. For example, ``--help`` will usually display a help text, ``--version`` will usually display the command version, and ``-i`` will often turn on interactive prompting before taking action.
.. _terminology:
@ -48,53 +48,51 @@ Terminology
Here we define some of the terms used on this page and throughout the rest of the fish documentation:
- **Argument**: A parameter given to a command. In ``echo foo``, the "foo" is an argument.
- **Argument**: A parameter given to a command.
- **Builtin**: A command that is implemented by the shell. Builtins are so closely tied to the operation of the shell that it is impossible to implement them as external commands. In ``echo foo``, the "echo" is a builtin.
- **Builtin**: A command that is implemented by the shell. Builtins are so closely tied to the operation of the shell that it is impossible to implement them as external commands.
- **Command**: A program that the shell can run, or more specifically an external program that the shell runs in another process. External commands are provided on your system, as executable files. In ``echo foo`` the "echo" is a builtin command, in ``command echo foo`` the "echo" is an external command, provided by a file like /bin/echo.
- **Command**: A program that the shell can run, or more specifically an external program that the shell runs in another process.
- **Function**: A block of commands that can be called as if they were a single command. By using functions, it is possible to string together multiple simple commands into one more advanced command.
- **Job**: A running pipeline or command.
- **Pipeline**: A set of commands strung together so that the output of one command is the input of the next command. ``echo foo | grep foo`` is a pipeline.
- **Pipeline**: A set of commands strung together so that the output of one command is the input of the next command.
- **Redirection**: An operation that changes one of the input or output streams associated with a job.
- **Switch** or **Option**: A special kind of argument that alters the behavior of a command. A switch almost always begins with one or two hyphens. In ``echo -n foo`` the "-n" is an option.
- **Switch** or **Option**: A special kind of argument that alters the behavior of a command. A switch almost always begins with one or two hyphens.
.. _quotes:
Quotes
------
Sometimes you want to give a command an argument that contains characters special to fish, like spaces or ``$`` or ``*``. To do that, you can use quotes::
rm "my file.txt"
to remove a file called ``my file.txt`` instead of trying to remove two files, ``my`` and ``file.txt``.
Fish understands two kinds of quotes: Single (``'``) and double (``"``), and both work slightly differently.
Between single quotes, fish performs no expansions. Between double quotes, fish only performs :ref:`variable expansion <expand-variable>` and :ref:`command substitution <expand-command-substitution>` in the ``$(command)``. No other kind of expansion (including :ref:`brace expansion <expand-brace>` or parameter expansion) is performed, and escape sequences (for example, ``\n``) are ignored. Within quotes, whitespace is not used to separate arguments, allowing quoted arguments to contain spaces.
Sometimes features like :ref:`parameter expansion <expand>` and :ref:`character escapes <escapes>` get in the way. When that happens, you can use quotes, either single (``'``) or double (``"``). Between single quotes, fish performs no expansions. Between double quotes, fish only performs :ref:`variable expansion <expand-variable>`. No other kind of expansion (including :ref:`brace expansion <expand-brace>` or parameter expansion) is performed, and escape sequences (for example, ``\n``) are ignored. Within quotes, whitespace is not used to separate arguments, allowing quoted arguments to contain spaces.
The only meaningful escape sequences in single quotes are ``\'``, which escapes a single quote and ``\\``, which escapes the backslash symbol. The only meaningful escapes in double quotes are ``\"``, which escapes a double quote, ``\$``, which escapes a dollar character, ``\`` followed by a newline, which deletes the backslash and the newline, and ``\\``, which escapes the backslash symbol.
Single quotes have no special meaning within double quotes and vice versa.
More examples::
Example::
rm "cumbersome filename.txt"
removes the file ``cumbersome filename.txt``, while
::
rm cumbersome filename.txt
removes two files, ``cumbersome`` and ``filename.txt``.
Another example::
grep 'enabled)$' foo.txt
searches for lines ending in ``enabled)`` in ``foo.txt`` (the ``$`` is special to ``grep``: it matches the end of the line).
::
apt install "postgres-*"
installs all packages with a name starting with "postgres-", instead of looking through the current directory for files named "postgres-something".
.. _escapes:
Escaping Characters
@ -176,9 +174,9 @@ The destination of a stream can be changed using something called *redirection*.
``DESTINATION`` can be one of the following:
- A filename to write the output to. Often ``>/dev/null`` to silence output by writing it to the special "sinkhole" file.
- A filename. The output will be written to the specified file. Often ``>/dev/null`` to silence output by writing it to the special "sinkhole" file.
- An ampersand (``&``) followed by the number of another file descriptor like ``&2`` for standard error. The output will be written to the destination descriptor.
- An ampersand followed by a minus sign (``&-``). The file descriptor will be closed. Note: This may cause the program to fail because its writes will be unsuccessful.
- An ampersand followed by a minus sign (``&-``). The file descriptor will be closed.
As a convenience, the redirection ``&>`` can be used to direct both stdout and stderr to the same destination. For example, ``echo hello &> all_output.txt`` redirects both stdout and stderr to the file ``all_output.txt``. This is equivalent to ``echo hello > all_output.txt 2>&1``.
@ -188,30 +186,7 @@ Any arbitrary file descriptor can be used in a redirection by prefixing the redi
- To redirect the output of descriptor N, use ``N>DESTINATION``.
- To append the output of descriptor N to a file, use ``N>>DESTINATION_FILE``.
For example::
# Write `foo`'s standard error (file descriptor 2)
# to a file called "output.stderr":
foo 2> output.stderr
# if $num doesn't contain a number,
# this test will be false and print an error,
# so by ignoring the error we can be sure that we're dealing
# with a number in the "if" block:
if test "$num" -gt 2 2>/dev/null
# do things with $num as a number greater than 2
else
# do things if $num is <= 2 or not a number
end
# Save `make`s output in a file:
make &>/log
# Redirections stack and can be used with blocks:
begin
echo stdout
echo stderr >&2 # <- this goes to stderr!
end >/dev/null # ignore stdout, so this prints "stderr"
For example, ``echo hello 2> output.stderr`` writes the standard error (file descriptor 2) to ``output.stderr``.
It is an error to redirect a builtin, function, or block to a file descriptor above 2. However this is supported for external commands.
@ -243,7 +218,7 @@ As a convenience, the pipe ``&|`` redirects both stdout and stderr to the same p
Job control
-----------
When you start a job in fish, fish itself will pause, and give control of the terminal to the program just started. Sometimes, you want to continue using the commandline, and have the job run in the background. To create a background job, append an ``&`` (ampersand) to your command. This will tell fish to run the job in the background. Background jobs are very useful when running programs that have a graphical user interface.
When you start a job in fish, fish itself will pause, and give control of the terminal to the program just started. Sometimes, you want to continue using the commandline, and have the job run in the background. To create a background job, append an \& (ampersand) to your command. This will tell fish to run the job in the background. Background jobs are very useful when running programs that have a graphical user interface.
Example::
@ -280,7 +255,7 @@ The first line tells fish to define a function by the name of ``ll``, so it can
Calling this as ``ll /tmp/`` will end up running ``ls -l /tmp/``, which will list the contents of /tmp.
This is a kind of function known as an :ref:`alias <syntax-aliases>`.
This is a kind of function known as a :ref:`wrapper <syntax-function-wrappers>` or "alias".
Fish's prompt is also defined in a function, called :doc:`fish_prompt <cmds/fish_prompt>`. It is run when the prompt is about to be displayed and its output forms the prompt::
@ -302,12 +277,12 @@ The :doc:`functions <cmds/functions>` builtin can show a function's current defi
For more information on functions, see the documentation for the :doc:`function <cmds/function>` builtin.
.. _syntax-aliases:
.. _syntax-function-wrappers:
Defining aliases
^^^^^^^^^^^^^^^^
One of the most common uses for functions is to slightly alter the behavior of an already existing command. For example, one might want to redefine the ``ls`` command to display colors. The switch for turning on colors on GNU systems is ``--color=auto``. An alias around ``ls`` might look like this::
One of the most common uses for functions is to slightly alter the behavior of an already existing command. For example, one might want to redefine the ``ls`` command to display colors. The switch for turning on colors on GNU systems is ``--color=auto``. An alias, or wrapper, around ``ls`` might look like this::
function ls
command ls --color=auto $argv
@ -319,7 +294,8 @@ There are a few important things that need to be noted about aliases:
- If the alias has the same name as the aliased command, you need to prefix the call to the program with ``command`` to tell fish that the function should not call itself, but rather a command with the same name. If you forget to do so, the function would call itself until the end of time. Usually fish is smart enough to figure this out and will refrain from doing so (which is hopefully in your interest).
To easily create a function of this form, you can use the :doc:`alias <cmds/alias>` command. Unlike other shells, this just makes functions - fish has no separate concept of an "alias", we just use the word for a simple wrapping function like this. :doc:`alias <cmds/alias>` immediately creates a function. Consider using ``alias --save`` or :doc:`funcsave <cmds/funcsave>` to save the created function into an autoload file instead of recreating the alias each time.
To easily create a function of this form, you can use the :doc:`alias <cmds/alias>` command. Unlike other shells, this just makes functions - fish has no separate concept of an "alias", we just use the word for a function wrapper like this. :doc:`alias <cmds/alias>` immediately creates a function. Consider using ``alias --save`` or :doc:`funcsave <cmds/funcsave>` to save the created function into an autoload file instead of recreating the alias each time.
For an alternative, try :ref:`abbreviations <abbreviations>`. These are words that are expanded while you type, instead of being actual functions inside the shell.
@ -379,122 +355,38 @@ Comments can also appear after a line like so::
Conditions
----------
Fish has some builtins that let you execute commands only if a specific criterion is met: :doc:`if <cmds/if>`, :doc:`switch <cmds/switch>`, :doc:`and <cmds/and>` and :doc:`or <cmds/or>`, and also the familiar :ref:`&&/|| <syntax-combiners>` syntax.
Fish has some builtins that let you execute commands only if a specific criterion is met: :doc:`if <cmds/if>`, :doc:`switch <cmds/switch>`, :doc:`and <cmds/and>` and :doc:`or <cmds/or>`, and also the familiar :ref:`&&/|| <tut-combiners>` syntax.
.. _syntax-if:
The :doc:`switch <cmds/switch>` command is used to execute one of possibly many blocks of commands depending on the value of a string. See the documentation for :doc:`switch <cmds/switch>` for more information.
The ``if`` statement
^^^^^^^^^^^^^^^^^^^^
The other conditionals use the :ref:`exit status <variables-status>` of a command to decide if a command or a block of commands should be executed.
The :doc:`if <cmds/if>` statement runs a block of commands if the condition was true.
Unlike programming languages you might know, :doc:`if <cmds/if>` doesn't take a *condition*, it takes a *command*. If that command returned a successful :ref:`exit status <variables-status>` (that's 0), the ``if`` branch is taken, otherwise the :doc:`else <cmds/else>` branch.
Like other shells, but unlike typical programming languages you might know, the condition here is a *command*. Fish runs it, and if it returns a true :ref:`exit status <variables-status>` (that's 0), the if-block is run. For example::
if test -e /etc/os-release
cat /etc/os-release
end
This uses the :doc:`test <cmds/test>` command to see if the file /etc/os-release exists. If it does, it runs ``cat``, which prints it on the screen.
Unlike other shells, the condition command just ends after the first job, there is no ``then`` here. Combiners like ``and`` and ``or`` extend the condition.
``if`` is commonly used with the :doc:`test <cmds/test>` command that can check conditions.::
To check a condition, there is the :doc:`test <cmds/test>` command::
if test 5 -gt 2
echo "Yes, 5 is greater than 2"
echo Yes, five is greater than two
end
``if`` can also take ``else if`` clauses with additional conditions and an :doc:`else <cmds/else>` clause that is executed when everything else was false::
if test "$number" -gt 10
echo Your number was greater than 10
else if test "$number" -gt 5
echo Your number was greater than 5
else if test "$number" -gt 1
echo Your number was greater than 1
else
echo Your number was smaller or equal to 1
end
The :doc:`not <cmds/not>` keyword can be used to invert the status::
Some examples::
# Just see if the file contains the string "fish" anywhere.
# This executes the `grep` command, which searches for a string,
# and if it finds it returns a status of 0.
# The `not` then turns 0 into 1 or anything else into 0.
# The `-q` switch stops it from printing any matches.
if not grep -q fish myanimals
echo "You don't have fish!"
else
if grep -q fish myanimals
echo "You have fish!"
else
echo "You don't have fish!"
end
The ``switch`` statement
^^^^^^^^^^^^^^^^^^^^^^^^
The :doc:`switch <cmds/switch>` command is used to execute one of possibly many blocks of commands depending on the value of a string. It can take multiple :doc:`case <cmds/case>` blocks that are executed when the string matches. They can take :ref:`wildcards <expand-wildcard>`. For example::
switch (uname)
case Linux
echo Hi Tux!
case Darwin
echo Hi Hexley!
case DragonFly '*BSD'
echo Hi Beastie! # this also works for FreeBSD and NetBSD
case '*'
echo Hi, stranger!
end
Unlike other shells or programming languages, there is no fallthrough - the first matching ``case`` block is executed and then control jumps out of the ``switch``.
.. _syntax-combiners:
Combiners (``and`` / ``or`` / ``&&`` / ``||``)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For simple checks, you can use combiners. :doc:`and <cmds/and>` or ``&&`` run the second command if the first succeeded, while :doc:`or <cmds/or>` or ``||`` run it if the first failed. For example::
# $XDG_CONFIG_HOME is a standard place to store configuration.
# If it's not set applications should use ~/.config.
set -q XDG_CONFIG_HOME; and set -l configdir $XDG_CONFIG_HOME
or set -l configdir ~/.config
Note that combiners are *lazy* - only the part that is necessary to determine the final status is run.
Compare::
if sleep 2; and false
echo 'How did I get here? This should be impossible'
end
and::
if false; and sleep 2
echo 'How did I get here? This should be impossible'
end
These do essentially the same thing, but the former takes 2 seconds longer because the ``sleep`` always needs to run.
Or you can have a case where it is necessary to stop early::
if command -sq foo; and foo
If this went on after seeing that the command "foo" doesn't exist, it would try to run ``foo`` and error because it wasn't found!
Combiners really just execute step-by-step, so it isn't recommended to build longer chains of them because they might do something you don't want. Consider::
test -e /etc/my.config
or echo "OH NO WE NEED A CONFIG FILE"
and return 1
This will execute ``return 1`` also if the ``test`` succeeded. This is because fish runs ``test -e /etc/my.config``, sets $status to 0, then skips the ``echo``, keeps $status at 0, and then executes the ``return 1`` because $status is still 0.
So if you have more complex conditions or want to run multiple things after something failed, consider using an :ref:`if <syntax-if>`. Here that would be::
if not test -e /etc/my.config
echo "OH NO WE NEED A CONFIG FILE"
return 1
end
For more, see the documentation for the builtins or the :ref:`Conditionals <tut-conditionals>` section of the tutorial.
.. _syntax-loops-and-blocks:
@ -552,11 +444,11 @@ Parameter expansion
When fish is given a commandline, it expands the parameters before sending them to the command. There are multiple different kinds of expansions:
- :ref:`Wildcards <expand-wildcard>`, to create filenames from patterns - ``*.jpg``
- :ref:`Variable expansion <expand-variable>`, to use the value of a variable - ``$HOME``
- :ref:`Command substitution <expand-command-substitution>`, to use the output of another command - ``$(cat /path/to/file)``
- :ref:`Brace expansion <expand-brace>`, to write lists with common pre- or suffixes in a shorter way ``{/usr,}/bin``
- :ref:`Tilde expansion <expand-home>`, to turn the ``~`` at the beginning of paths into the path to the home directory ``~/bin``
- :ref:`Wildcards <expand-wildcard>`, to create filenames from patterns
- :ref:`Variable expansion <expand-variable>`, to use the value of a variable
- :ref:`Command substitution <expand-command-substitution>`, to use the output of another command
- :ref:`Brace expansion <expand-brace>`, to write lists with common pre- or suffixes in a shorter way
- :ref:`Tilde expansion <expand-home>`, to turn the ``~`` at the beginning of paths into the path to the home directory
Parameter expansion is limited to 524288 items. There is a limit to how many arguments the operating system allows for any command, and 524288 is far above it. This is a measure to stop the shell from hanging doing useless computation.
@ -607,7 +499,7 @@ Unlike bash (by default), fish will not pass on the literal glob character if no
Variable expansion
^^^^^^^^^^^^^^^^^^
One of the most important expansions in fish is the "variable expansion". This is the replacing of a dollar sign (``$``) followed by a variable name with the _value_ of that variable.
One of the most important expansions in fish is the "variable expansion". This is the replacing of a dollar sign (``$``) followed by a variable name with the _value_ of that variable. For more on shell variables, read the :ref:`Shell variables <variables>` section.
In the simplest case, this is just something like::
@ -625,6 +517,7 @@ For more on how setting variables works, see :ref:`Shell variables <variables>`
Sometimes a variable has no value because it is undefined or empty, and it expands to nothing::
echo $nonexistentvariable
# Prints no output.
@ -642,13 +535,10 @@ The latter syntax ``{$WORD}`` is a special case of :ref:`brace expansion <expand
If $WORD here is undefined or an empty list, the "s" is not printed. However, it is printed if $WORD is the empty string (like after ``set WORD ""``).
For more on shell variables, read the :ref:`Shell variables <variables>` section.
Quoting variables
'''''''''''''''''
Unlike all the other expansions, variable expansion also happens in double quoted strings. Inside double quotes (``"these"``), variables will always expand to exactly one argument. If they are empty or undefined, it will result in an empty string. If they have one element, they'll expand to that element. If they have more than that, the elements will be joined with spaces, unless the variable is a :ref:`path variable <variables-path>` - in that case it will use a colon (``:``) instead [#]_.
Unlike all the other expansions, variable expansion also happens in double quoted strings. Inside double quotes (``"these"``), variables will always expand to exactly one argument. If they are empty or undefined, it will result in an empty string. If they have one element, they'll expand to that element. If they have more than that, the elements will be joined with spaces, unless the variable is a :ref:`path variable <variables-path>` - in that case it will use a colon (`:`) instead [#]_.
Outside of double quotes, variables will expand to as many arguments as they have elements. That means an empty list will expand to nothing, a variable with one element will expand to that element, and a variable with multiple elements will expand to each of those elements separately.
@ -693,49 +583,28 @@ The ``$`` symbol can also be used multiple times, as a kind of "dereference" ope
# 20
# 30
``$$foo[$i]`` is "the value of the variable named by ``$foo[$i]``".
``$$foo[$i]`` is "the value of the variable named by ``$foo[$i]``.
When using this feature together with list brackets, the brackets will be used from the inside out. ``$$foo[5]`` will use the fifth element of ``$foo`` as a variable name, instead of giving the fifth element of all the variables $foo refers to. That would instead be expressed as ``$$foo[1..-1][5]`` (take all elements of ``$foo``, use them as variable names, then give the fifth element of those).
Some more examples::
set listone 1 2 3
set listtwo 4 5 6
set var listone listtwo
echo $$var
# Output is 1 2 3 4 5 6
echo $$var[1]
# Output is 1 2 3
echo $$var[2][3]
# $var[1] is listtwo, third element of that is 6, output is 6
echo $$var[..][2]
# The second element of every variable, so output is
# 2 5
.. _expand-command-substitution:
Command substitution
^^^^^^^^^^^^^^^^^^^^
A ``command substitution`` is an expansion that uses the *output* of a command as the arguments to another. For example::
The output of a command (or an entire :ref:`pipeline <pipes>`) can be used as the arguments to another command.
echo (pwd)
When you write a command in parentheses like ``outercommand (innercommand)``, the ``innercommand`` will be executed first. Its output will be taken and each line given as a separate argument to ``outercommand``, which will then be executed. [#]_
This executes the :doc:`pwd <cmds/pwd>` command, takes its output (more specifically what it wrote to the standard output "stdout" stream) and uses it as arguments to :doc:`echo <cmds/echo>`. So the inner command (the ``pwd``) is run first and has to complete before the outer command can even be started.
If the inner command prints multiple lines, fish will use each separate line as a separate argument to the outer command. Unlike other shells, the value of ``$IFS`` is not used [#]_, fish splits on newlines.
A command substitution can also be spelled with a dollar sign like ``outercommand $(innercommand)``. This variant is also allowed inside double quotes. When using double quotes, the command output is not split up by lines, but trailing empty lines are still removed.
A command substitution can have a dollar sign before the opening parenthesis like ``outercommand $(innercommand)``. This variant is also allowed inside double quotes. When using double quotes, the command output is not split up by lines.
If the output is piped to :doc:`string split or string split0 <cmds/string-split>` as the last step, those splits are used as they appear instead of splitting lines.
The exit status of the last run command substitution is available in the :ref:`status <variables-status>` variable if the substitution happens in the context of a :doc:`set <cmds/set>` command (so ``if set -l (something)`` checks if ``something`` returned true).
To use only some lines of the output, refer to :ref:`slices <expand-slices>`.
To use only part of the output, refer to :ref:`index range expansion <expand-index-range>`.
Fish has a default limit of 100 MiB on the data it will read in a command sustitution. If that limit is reached the command (all of it, not just the command substitution - the outer command won't be executed at all) fails and ``$status`` is set to 122. This is so command substitutions can't cause the system to go out of memory, because typically your operating system has a much lower limit, so reading more than that would be useless and harmful. This limit can be adjusted with the ``fish_read_limit`` variable (`0` meaning no limit). This limit also affects the :doc:`read <cmds/read>` command.
Examples::
@ -753,6 +622,7 @@ Examples::
# Set ``$data`` to the contents of data, splitting on NUL-bytes.
set data (cat data | string split0)
Sometimes you want to pass the output of a command to another command that only accepts files. If it's just one file, you can usually just pass it via a pipe, like::
grep fish myanimallist1 | wc -l
@ -764,9 +634,7 @@ but if you need multiple or the command doesn't read from standard input, "proce
This creates a temporary file, stores the output of the command in that file and prints the filename, so it is given to the outer command.
Fish has a default limit of 100 MiB on the data it will read in a command sustitution. If that limit is reached the command (all of it, not just the command substitution - the outer command won't be executed at all) fails and ``$status`` is set to 122. This is so command substitutions can't cause the system to go out of memory, because typically your operating system has a much lower limit, so reading more than that would be useless and harmful. This limit can be adjusted with the ``fish_read_limit`` variable (`0` meaning no limit). This limit also affects the :doc:`read <cmds/read>` command.
.. [#] One exception: Setting ``$IFS`` to empty will disable line splitting. This is deprecated, use :doc:`string split <cmds/string-split>` instead.
.. [#] Setting ``$IFS`` to empty will disable line splitting. This is deprecated, use :doc:`string split <cmds/string-split>` instead.
.. _expand-brace:
@ -817,7 +685,7 @@ To use a "," as an element, :ref:`quote <quotes>` or :ref:`escape <escapes>` it.
Combining lists (Cartesian Product)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When lists are expanded with other parts attached, they are expanded with these parts still attached. Even if two lists are attached to each other, they are expanded in all combinations. This is referred to as the "cartesian product" (like in mathematics), and works basically like :ref:`brace expansion <expand-brace>`.
When lists are expanded with other parts attached, they are expanded with these parts still attached. Even if two lists are attached to each other, they are expanded in all combinations. This is referred to as the `cartesian product` (like in mathematics), and works basically like :ref:`brace expansion <expand-brace>`.
Examples::
@ -877,10 +745,10 @@ This can be quite useful. For example, if you want to go through all the files i
Because :envvar:`PATH` is a list, this expands to all the files in all the directories in it. And if there are no directories in :envvar:`PATH`, the right answer here is to expand to no files.
.. _expand-slices:
.. _expand-index-range:
Slices
^^^^^^
Index range expansion
^^^^^^^^^^^^^^^^^^^^^
Sometimes it's necessary to access only some of the elements of a :ref:`list <variables-lists>` (all fish variables are lists), or some of the lines a :ref:`command substitution <expand-command-substitution>` outputs. Both are possible in fish by writing a set of indices in brackets, like::
@ -900,7 +768,7 @@ If a list has 5 elements the indices go from 1 to 5, so a range of ``2..16`` wil
If the end is negative the range always goes up, so ``2..-2`` will go from element 2 to 4, and ``2..-16`` won't go anywhere because there is no way to go from the second element to one that doesn't exist, while going up.
If the start is negative the range always goes down, so ``-2..1`` will go from element 4 to 1, and ``-16..2`` won't go anywhere because there is no way to go from an element that doesn't exist to the second element, while going down.
A missing starting index in a range defaults to 1. This is allowed if the range is the first index expression of the sequence. Similarly, a missing ending index, defaulting to -1 is allowed for the last index in the sequence.
A missing starting index in a range defaults to 1. This is allowed if the range is the first index expression of the sequence. Similarly, a missing ending index, defaulting to -1 is allowed for the last index range in the sequence.
Multiple ranges are also possible, separated with a space.
@ -979,6 +847,7 @@ The ``~`` (tilde) character at the beginning of a parameter, followed by a usern
echo ~root # prints root's home directory, probably "/root"
.. _combine:
Combining different expansions
@ -1030,8 +899,8 @@ Variable Scope
There are four kinds of variables in fish: universal, global, function and local variables.
- Universal variables are shared between all fish sessions a user is running on one computer. They are stored on disk and persist even after reboot.
- Global variables are specific to the current fish session. They can be erased by explicitly requesting ``set -e``.
- Universal variables are shared between all fish sessions a user is running on one computer.
- Global variables are specific to the current fish session, and will never be erased unless explicitly requested by using ``set -e``.
- Function variables are specific to the currently executing function. They are erased ("go out of scope") when the current function ends. Outside of a function, they don't go out of scope.
- Local variables are specific to the current block of commands, and automatically erased when a specific block goes out of scope. A block of commands is a series of commands that begins with one of the commands ``for``, ``while`` , ``if``, ``function``, ``begin`` or ``switch``, and ends with the command ``end``. Outside of a block, this is the same as the function scope.
@ -1045,6 +914,7 @@ Variables can be explicitly set to be universal with the ``-U`` or ``--universal
There can be many variables with the same name, but different scopes. When you :ref:`use a variable <expand-variable>`, the smallest scoped variable of that name will be used. If a local variable exists, it will be used instead of the global or universal variable of the same name.
Example:
There are a few possible uses for different scopes.
@ -1099,33 +969,14 @@ Here is an example of local vs function-scoped variables::
set gnu "In the beginning there was nothing, which exploded"
end
# This will not output anything, since the pirate was local
echo $pirate
# This will output the good Captain's speech
# since $captain had function-scope.
# This will not output anything, since the pirate was local
echo $captain
# This will output Sir Terry's wisdom.
# This will output the good Captain's speech since $captain had function-scope.
echo $gnu
# Will output Sir Terry's wisdom.
end
When a function calls another, local variables aren't visible::
function shiver
set phrase 'Shiver me timbers'
end
function avast
set --local phrase 'Avast, mateys'
# Calling the shiver function here can not
# change any variables in the local scope
# so phrase remains as we set it here.
shiver
echo $phrase
end
avast
# Outputs "Avast, mateys"
When in doubt, use function-scoped variables. When you need to make a variable accessible everywhere, make it global. When you need to persistently store configuration, make it universal. When you want to use a variable only in a short block, make it local.
.. _variables-override:
@ -1142,8 +993,7 @@ If you want to override a variable for a single command, you can use "var=val" s
Unlike other shells, fish will first set the variable and then perform other expansions on the line, so::
set foo banana
foo=gagaga echo $foo
# prints gagaga, while in other shells it might print "banana"
foo=gagaga echo $foo # prints gagaga, while in other shells it might print "banana"
Multiple elements can be given in a :ref:`brace expansion<expand-brace>`::
@ -1162,8 +1012,8 @@ This syntax is supported since fish 3.1.
.. _variables-universal:
Universal Variables
^^^^^^^^^^^^^^^^^^^
More on universal variables
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Universal variables are variables that are shared between all the user's fish sessions on the computer. Fish stores many of its configuration options as universal variables. This means that in order to change fish settings, all you have to do is change the variable value once, and it will be automatically updated for all sessions, and preserved across computer reboots and login/logout.
@ -1173,16 +1023,53 @@ To see universal variables in action, start two fish sessions side by side, and
Do not append to universal variables in :ref:`config.fish <configuration>`, because these variables will then get longer with each new shell instance. Instead, simply set them once at the command line.
.. _variables-functions:
Variable scope for functions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When calling a function, all current local variables temporarily disappear. This shadowing of the local scope is needed since the variable namespace would become cluttered, making it very easy to accidentally overwrite variables from another function.
For example::
function shiver
set phrase 'Shiver me timbers'
end
function avast
set --local phrase 'Avast, mateys'
# Calling the shiver function here can not
# change any variables in the local scope
shiver
echo $phrase
end
avast
# Outputs "Avast, mateys"
.. _variables-export:
Exporting variables
^^^^^^^^^^^^^^^^^^^
Variables in fish can be exported, so they will be inherited by any commands started by fish. In particular, this is necessary for variables used to configure external commands like ``PAGER`` or ``GOPATH``, but also for variables that contain general system settings like ``PATH`` or ``LANGUAGE``. If an external command needs to know a variable, it needs to be exported. Exported variables are also often called "environment variables".
Variables in fish can be "exported", so they will be inherited by any commands started by fish. In particular, this is necessary for variables used to configure external commands like ``LESS`` or ``GOPATH``, but also for variables that contain general system settings like ``PATH`` or ``LANGUAGE``. If an external command needs to know a variable, it needs to be exported.
This also applies to fish - when it starts up, it receives environment variables from its parent (usually the terminal). These typically include system configuration like :envvar:`PATH` and :ref:`locale variables <variables-locale>`.
Variables can be explicitly set to be exported with the ``-x`` or ``--export`` switch, or not exported with the ``-u`` or ``--unexport`` switch. The exporting rules when setting a variable are similar to the scoping rules for variables - when an option is passed it is respected, otherwise the variable's existing state is used. If no option is passed and the variable didn't exist yet it is not exported.
Variables can be explicitly set to be exported with the ``-x`` or ``--export`` switch, or not exported with the ``-u`` or ``--unexport`` switch. The exporting rules when setting a variable are identical to the scoping rules for variables:
- If a variable is explicitly set to either be exported or not exported, that setting will be honored.
- If a variable is not explicitly set to be exported or not exported, but has been previously defined, the previous exporting rule for the variable is kept.
- Otherwise, by default, the variable will not be exported.
- If a variable has function or local scope and is exported, any function called receives a *copy* of it, so any changes it makes to the variable disappear once the function returns.
- Global variables are accessible to functions whether they are exported or not.
As a naming convention, exported variables are in uppercase and unexported variables are in lowercase.
@ -1195,7 +1082,7 @@ For example::
set -gx GTK2_RC_FILES "$XDG_CONFIG_HOME/gtk-2.0/gtkrc"
set -gx LESSHISTFILE "-"
Note: Exporting is not a :ref:`scope <variables-scope>`, but an additional state. It typically makes sense to make exported variables global as well, but local-exported variables can be useful if you need something more specific than :ref:`Overrides <variables-override>`. They are *copied* to functions so the function can't alter them outside, and still available to commands. Global variables are accessible to functions whether they are exported or not.
Note: Exporting is not a :ref:`scope <variables-scope>`, but an additional state. It typically makes sense to make exported variables global as well, but local-exported variables can be useful if you need something more specific than :ref:`Overrides <variables-override>`. They are *copied* to functions so the function can't alter them outside, and still available to commands.
.. _variables-lists:
@ -1242,6 +1129,7 @@ It is also possible to set or erase individual elements of a list::
# Output 'evil'
echo $smurf
If you specify a negative index when expanding or assigning to a list variable, the index will be taken from the *end* of the list. For example, the index -1 is the last element of the list::
> set fruit apple orange banana
@ -1257,7 +1145,7 @@ If you specify a negative index when expanding or assigning to a list variable,
orange
apple
As you see, you can use a range of indices, see :ref:`slices <expand-slices>` for details.
As you see, you can use a range of indices, see :ref:`index range expansion <expand-index-range>` for details.
All lists are one-dimensional and can't contain other lists, although it is possible to fake nested lists using dereferencing - see :ref:`variable expansion <expand-variable>`.
@ -1273,15 +1161,11 @@ Fish automatically creates lists from all environment variables whose name ends
Lists can be inspected with the :doc:`count <cmds/count>` or the :doc:`contains <cmds/contains>` commands::
> count $smurf
2
count $smurf
# 2
> contains blue $smurf
# blue was found, so it exits with status 0
# (without printing anything)
> echo $status
0
contains blue $smurf
# key found, exits with status 0
> contains -i blue $smurf
1
@ -1315,31 +1199,23 @@ This function takes whatever arguments it gets and prints the first and third::
apple
banana
That covers the positional arguments, but commandline tools often get various options and flags, and $argv would contain them intermingled with the positional arguments. Typical unix argument handling allows short options (``-h``, also grouped like in ``ls -lah``), long options (``--help``) and allows those options to take arguments (``--color=auto`` or ``--position anywhere`` or ``complete -C"git "``) as well as a ``--`` separator to signal the end of options. Handling all of these manually is tricky and error-prone.
Commandline tools often get various options and flags and positional arguments, and $argv would contain all of these.
A more robust approach to option handling is :doc:`argparse <cmds/argparse>`, which checks the defined options and puts them into various variables, leaving only the positional arguments in $argv. Here's a simple example::
A more robust approach to argument handling is :doc:`argparse <cmds/argparse>`, which checks the defined options and puts them into various variables, leaving only the positional arguments in $argv. Here's a simple example::
function mybetterfunction
# We tell argparse about -h/--help and -s/--second
# - these are short and long forms of the same option.
# The "--" here is mandatory,
# it tells it from where to read the arguments.
argparse h/help s/second -- $argv
# exit if argparse failed because
# it found an option it didn't recognize
# - it will print an error
or return
or return # exit if argparse failed because it found an option it didn't recognize - it will print an error
# If -h or --help is given, we print a little help text and return
if set -ql _flag_help
if set -q _flag_help
echo "mybetterfunction [-h|--help] [-s|--second] [ARGUMENT ...]"
return 0
end
# If -s or --second is given, we print the second argument,
# not the first and third.
# (this is also available as _flag_s because of the short version)
if set -ql _flag_second
if set -q _flag_second
echo $argv[2]
else
echo $argv[1]
@ -1352,14 +1228,12 @@ The options will be *removed* from $argv, so $argv[2] is the second *positional*
> mybetterfunction first -s second third
second
For more information on argparse, like how to handle option arguments, see :doc:`the argparse documentation <cmds/argparse>`.
.. _variables-path:
PATH variables
^^^^^^^^^^^^^^
Path variables are a special kind of variable used to support colon-delimited path lists including :envvar:`PATH`, :envvar:`CDPATH`, :envvar:`MANPATH`, :envvar:`PYTHONPATH`, etc. All variables that end in "PATH" (case-sensitive) become PATH variables by default.
Path variables are a special kind of variable used to support colon-delimited path lists including :envvar:`PATH`, :envvar:`CDPATH`, :envvar:`MANPATH`, :envvar:`PYTHONPATH`, etc. All variables that end in "PATH" (case-sensitive) become PATH variables.
PATH variables act as normal lists, except they are implicitly joined and split on colons.
@ -1395,12 +1269,20 @@ You can change the settings of fish by changing the values of certain variables.
.. envvar:: PATH
A list of directories in which to search for commands. This is a common unix variable also used by other tools.
A list of directories in which to search for commands.
.. envvar:: CDPATH
A list of directories in which the :doc:`cd <cmds/cd>` builtin looks for a new directory.
.. envvar:: FISH_DEBUG
Controls which debug categories :command:`fish` enables for output, analogous to the ``--debug`` option.
.. envvar:: FISH_DEBUG_OUTPUT
Specifies a file to direct debug output to.
.. describe:: Locale Variables
The locale variables :envvar:`LANG`, :envvar:`LC_ALL`, :envvar:`LC_COLLATE`, :envvar:`LC_CTYPE`, :envvar:`LC_MESSAGES`, :envvar:`LC_MONETARY`, :envvar:`LC_NUMERIC`, and :envvar:`LANG` set the language option for the shell and subprograms. See the section :ref:`Locale variables <variables-locale>` for more information.
@ -1409,16 +1291,6 @@ You can change the settings of fish by changing the values of certain variables.
A number of variable starting with the prefixes ``fish_color`` and ``fish_pager_color``. See :ref:`Variables for changing highlighting colors <variables-color>` for more information.
.. envvar:: fish_term24bit
If this is set to 1, fish will assume the terminal understands 24-bit RGB color sequences, and won't translate them to the 256 or 16 color palette.
This is often detected automatically.
.. envvar:: fish_term256
If this is set to 1, fish will assume the terminal understands 256 colors, and won't translate matching colors down to the 16 color palette.
This is usually autodetected.
.. envvar:: fish_ambiguous_width
controls the computed width of ambiguous-width characters. This should be set to 1 if your terminal renders these characters as single-width (typical), or 2 if double-width.
@ -1443,18 +1315,6 @@ You can change the settings of fish by changing the values of certain variables.
sets how long fish waits for another key after seeing an escape, to distinguish pressing the escape key from the start of an escape sequence. The default is 30ms. Increasing it increases the latency but allows pressing escape instead of alt for alt+character bindings. For more information, see :ref:`the chapter in the bind documentation <cmd-bind-escape>`.
.. envvar:: fish_complete_path
determines where fish looks for completion. When trying to complete for a command, fish looks for files in the directories in this variable.
.. envvar:: fish_cursor_selection_mode
controls whether the selection is inclusive or exclusive of the character under the cursor (see :ref:`Copy and Paste <killring>`).
.. envvar:: fish_function_path
determines where fish looks for functions. When fish :ref:`autoloads <syntax-function-autoloading>` a function, it will look for files in these directories.
.. envvar:: fish_greeting
the greeting message printed on startup. This is printed by a function of the same name that can be overridden for more complicated changes (see :doc:`funced <cmds/funced>`)
@ -1472,14 +1332,6 @@ You can change the settings of fish by changing the values of certain variables.
if set and not empty, will cause fish to print commands before they execute, similar to ``set -x``
in bash. The trace is printed to the path given by the `--debug-output` option to fish or the :envvar:`FISH_DEBUG_OUTPUT` variable. It goes to stderr by default.
.. envvar:: FISH_DEBUG
Controls which debug categories :command:`fish` enables for output, analogous to the ``--debug`` option.
.. envvar:: FISH_DEBUG_OUTPUT
Specifies a file to direct debug output to.
.. envvar:: fish_user_paths
a list of directories that are prepended to :envvar:`PATH`. This can be a universal variable.
@ -1518,10 +1370,6 @@ Fish also provides additional information through the values of certain environm
a list of entries in fish's :ref:`kill ring <killring>` of cut text.
.. envvar:: fish_read_limit
how many bytes fish will process with :doc:`read <cmds/read>` or in a :ref:`command substitution <expand-command-substitution>`.
.. envvar:: fish_pid
the process ID (PID) of the shell.
@ -1566,12 +1414,6 @@ Fish also provides additional information through the values of certain environm
the "generation" count of ``$status``. This will be incremented only when the previous command produced an explicit status. (For example, background jobs will not increment this).
.. ENVVAR:: TERM
the type of the current terminal. When fish tries to determine how the terminal works - how many colors it supports, what sequences it sends for keys and other things - it looks at this variable and the corresponding information in the terminfo database (see ``man terminfo``).
Note: Typically this should not be changed as the terminal sets it to the correct value.
.. ENVVAR:: USER
the current username. This variable can be changed.
@ -1766,8 +1608,7 @@ Let's make up an example. This function will :ref:`glob <expand-wildcard>` the f
# If there are more than 5 files
if test (count $files) -gt 5
# and both stdin (for reading input)
# and stdout (for writing the prompt)
# and both stdin (for reading input) and stdout (for writing the prompt)
# are terminals
and isatty stdin
and isatty stdout
@ -1951,6 +1792,4 @@ Fish includes basic built-in debugging facilities that allow you to stop executi
To start a debug session simply insert the :doc:`builtin command <cmds/breakpoint>` ``breakpoint`` at the point in a function or script where you wish to gain control, then run the function or script. Also, the default action of the ``TRAP`` signal is to call this builtin, meaning a running script can be actively debugged by sending it the ``TRAP`` signal (``kill -s TRAP <PID>``). There is limited support for interactively setting or modifying breakpoints from this debug prompt: it is possible to insert new breakpoints in (or remove old ones from) other functions by using the ``funced`` function to edit the definition of a function, but it is not possible to add or remove a breakpoint from the function/script currently loaded and being executed.
Another way to debug script issues is to set the :envvar:`fish_trace` variable, e.g. ``fish_trace=1 fish_prompt`` to see which commands fish executes when running the :doc:`fish_prompt <cmds/fish_prompt>` function.
If you specifically want to debug performance issues, :program:`fish` can be run with the ``--profile /path/to/profile.log`` option to save a profile to the specified path. This profile log includes a breakdown of how long each step in the execution took. See :doc:`fish <cmds/fish>` for more information.

View File

@ -4,7 +4,7 @@ License
License for fish
----------------
``fish`` Copyright © 2005-2009 Axel Liljencrantz, 2009-2023 fish-shell contributors. ``fish`` is released under the GNU General Public License, version 2.
``fish`` Copyright © 2005-2009 Axel Liljencrantz, 2009-2022 fish-shell contributors. ``fish`` is released under the GNU General Public License, version 2.
``fish`` includes other code licensed under the GNU General Public License, version 2, including GNU ``printf``.

View File

@ -169,7 +169,7 @@ div.warning {
border: 1px solid #f66;
}
div.admonition, div.versionchanged {
div.admonition {
padding: 7px;
}
@ -177,7 +177,7 @@ p.admonition-title::after {
content: ":";
}
p.admonition-title, span.versionmodified {
p.admonition-title {
display: inline;
font-weight: bold;
}
@ -352,7 +352,7 @@ div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 {
padding: 0.3em 0;
}
div.body h3, div.body h4 {
div.body h3 {
font-size: 140%;
}

View File

@ -77,9 +77,6 @@ Run ``help`` to open fish's help in a web browser, and ``man`` with the page (li
set - handle shell variables
Synopsis...
To open this section, use ``help getting-help``.
Fish works by running commands, which are often also installed on your computer. Usually these commands also provide help in the man system, so you can get help for them there. Try ``man ls`` to get help on your computer's ``ls`` command.
Syntax Highlighting
-------------------
@ -143,7 +140,7 @@ You can include multiple wildcards::
lesson.pdf
The recursive wildcard ``**`` searches directories recursively::
Especially powerful is the recursive wildcard ** which searches directories recursively::
> ls /var/**.log
/var/log/system.log
@ -162,15 +159,19 @@ You can pipe between commands with the usual vertical bar::
> echo hello world | wc
1 2 12
stdin and stdout can be redirected via the familiar ``<`` and ``>``. stderr is redirected with a ``2>``.
::
> grep fish < /etc/shells > ~/output.txt 2> ~/errors.txt
To redirect stdout and stderr into one file, you can use ``&>``::
> make &> make_output.txt
To redirect stdout and stderr into one file, you need to first redirect stdout, and then stderr into stdout::
> make > make_output.txt 2>&1
For more, see :ref:`Input and output redirections <redirects>` and :ref:`Pipes <pipes>`.
@ -226,10 +227,10 @@ If there's more than one possibility, it will list them:
:class: highlight
:prompt:`>` :red:`~/stuff/s`:kbd:`Tab`
~/stuff/script.sh :gray:`(command)` ~/stuff/sources/ :gray:`(directory)`
~/stuff/script.sh (Executable, 4.8kB) ~/stuff/sources/ (Directory)
Hit tab again to cycle through the possibilities. The part in parentheses there (that "command" and "directory") is the completion description. It's just a short hint to explain what kind of argument it is.
Hit tab again to cycle through the possibilities.
fish can also complete many commands, like git branches:
@ -238,7 +239,8 @@ fish can also complete many commands, like git branches:
:prompt:`>` :command:`git` :param:`merge pr`:kbd:`Tab` => :command:`git` :param:`merge prompt_designer`
:prompt:`>` :command:`git` :param:`checkout b`:kbd:`Tab`
builtin_list_io_merge :gray:`(Branch)` builtin_set_color :gray:`(Branch)` busted_events :gray:`(Tag)`
builtin_list_io_merge (Branch) builtin_set_color (Branch) busted_events (Tag)
Try hitting tab and see what fish can do!
@ -250,6 +252,7 @@ Like other shells, a dollar sign followed by a variable name is replaced with th
> echo My home directory is $HOME
My home directory is /home/tutorial
This is known as variable substitution, and it also happens in double quotes, but not single quotes::
> echo "My current directory is $PWD"
@ -257,7 +260,8 @@ This is known as variable substitution, and it also happens in double quotes, bu
> echo 'My current directory is $PWD'
My current directory is $PWD
Unlike other shells, fish has an ordinary command to set variables: ``set``, which takes a variable name, and then its value.
Unlike other shells, fish has no dedicated ``VARIABLE=VALUE`` syntax for setting variables. Instead it has an ordinary command: ``set``, which takes a variable name, and then its value.
::
@ -274,6 +278,7 @@ Unlike other shells, variables are not further split after substitution::
> ls
Mister Noodle
In bash, this would have created two directories "Mister" and "Noodle". In fish, it created only one: the variable had the value "Mister Noodle", so that is the argument that was passed to ``mkdir``, spaces and all.
You can erase (or "delete") a variable with ``-e`` or ``--erase``
@ -293,7 +298,7 @@ Exports (Shell Variables)
Sometimes you need to have a variable available to an external command, often as a setting. For example many programs like ``git`` or ``man`` read the ``$PAGER`` variable to figure out your preferred pager (the program that lets you scroll text). Other variables used like this include ``$BROWSER``, ``$LANG`` (to configure your language) and ``$PATH``. You'll note these are written in ALLCAPS, but that's just a convention.
To give a variable to an external command, it needs to be "exported". This is done with a flag to ``set``, either ``--export`` or just ``-x``.
To give a variable to an external command, it needs to be "exported". Unlike other shells, fish does not have an export command. Instead, a variable is exported via an option to ``set``, either ``--export`` or just ``-x``.
::
@ -402,7 +407,7 @@ Command substitutions without a dollar are not expanded within quotes, so the ve
> ls *.txt
testing_1360099791.txt
Unlike other shells, fish does not split command substitutions on any whitespace (like spaces or tabs), only newlines. Usually this is a big help because unix commands operate on a line-by-line basis. Sometimes it can be an issue with commands like ``pkg-config`` that print what is meant to be multiple arguments on a single line. To split it on spaces too, use ``string split``.
Unlike other shells, fish does not split command substitutions on any whitespace (like spaces or tabs), only newlines. This can be an issue with commands like ``pkg-config`` that print what is meant to be multiple arguments on a single line. To split it on spaces too, use ``string split``.
::
@ -439,15 +444,15 @@ To write them on the same line, use the semicolon (";"). That means the followin
echo fish
echo chips
This is useful interactively to enter multiple commands. In a script it's easier to read if the commands are on separate lines.
Exit Status
-----------
When a command exits, it returns a status code as a non-negative integer (that's a whole number >= 0).
When a command exits, it returns a status code as a non-negative integer.
Unlike other shells, fish stores the exit status of the last command in ``$status`` instead of ``$?``.
::
> false
@ -492,6 +497,7 @@ Conditionals (If, Else, Switch)
Use :doc:`if <cmds/if>` and :doc:`else <cmds/else>` to conditionally execute code, based on the exit status of a command.
::
if grep fish /etc/shells
@ -502,8 +508,10 @@ Use :doc:`if <cmds/if>` and :doc:`else <cmds/else>` to conditionally execute cod
echo Got nothing
end
To compare strings or numbers or check file properties (whether a file exists or is writeable and such), use :doc:`test <cmds/test>`, like
::
if test "$fish" = "flounder"
@ -530,12 +538,14 @@ To compare strings or numbers or check file properties (whether a file exists or
:ref:`Combiners <tut-combiners>` can also be used to make more complex conditions, like
::
if command -sq fish; and grep fish /etc/shells
if grep fish /etc/shells; and command -sq fish
echo fish is installed and configured
end
For even more complex conditions, use :doc:`begin <cmds/begin>` and :doc:`end <cmds/end>` to group parts of them.
There is also a :doc:`switch <cmds/switch>` command::
@ -568,6 +578,7 @@ A fish function is a list of commands, which may optionally take arguments. Unli
say_hello everybody!
# prints: Hello everybody!
Unlike other shells, fish does not have aliases or special prompt syntax. Functions take their place. [#]_
You can list the names of all functions with the :doc:`functions <cmds/functions>` builtin (note the plural!). fish starts out with a number of functions::
@ -575,6 +586,8 @@ You can list the names of all functions with the :doc:`functions <cmds/functions
> functions
N_, abbr, alias, bg, cd, cdh, contains_seq, dirh, dirs, disown, down-or-search, edit_command_buffer, export, fg, fish_add_path, fish_breakpoint_prompt, fish_clipboard_copy, fish_clipboard_paste, fish_config, fish_default_key_bindings, fish_default_mode_prompt, fish_git_prompt, fish_hg_prompt, fish_hybrid_key_bindings, fish_indent, fish_is_root_user, fish_job_summary, fish_key_reader, fish_md5, fish_mode_prompt, fish_npm_helper, fish_opt, fish_print_git_action, fish_print_hg_root, fish_prompt, fish_sigtrap_handler, fish_svn_prompt, fish_title, fish_update_completions, fish_vcs_prompt, fish_vi_cursor, fish_vi_key_bindings, funced, funcsave, grep, help, history, hostname, isatty, kill, la, ll, ls, man, nextd, open, popd, prevd, prompt_hostname, prompt_pwd, psub, pushd, realpath, seq, setenv, suspend, trap, type, umask, up-or-search, vared, wait
You can see the source for any function by passing its name to ``functions``::
> functions ls
@ -600,12 +613,14 @@ While loops::
# Loop forever
# yes, this really will loop forever. Unless you abort it with ctrl-c.
For loops can be used to iterate over a list. For example, a list of files::
for file in *.txt
cp $file $file.bak
end
Iterating over a list of numbers can be done with ``seq``::
for x in (seq 5)

View File

@ -1,38 +0,0 @@
FROM arm64v8/ubuntu:focal
LABEL org.opencontainers.image.source=https://github.com/fish-shell/fish-shell
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get -y install \
build-essential \
cmake \
clang-9 \
gettext \
git \
libncurses5-dev \
libpcre2-dev \
locales \
ninja-build \
python3 \
python3-pexpect \
sudo \
&& locale-gen en_US.UTF-8 \
&& apt-get clean
RUN groupadd -g 1000 fishuser \
&& useradd -p $(openssl passwd -1 fish) -d /home/fishuser -m -u 1000 -g 1000 fishuser \
&& adduser fishuser sudo \
&& mkdir -p /home/fishuser/fish-build \
&& mkdir /fish-source \
&& chown -R fishuser:fishuser /home/fishuser /fish-source
USER fishuser
WORKDIR /home/fishuser
COPY fish_run_tests.sh /
CMD /fish_run_tests.sh

View File

@ -1,41 +0,0 @@
FROM arm32v7/ubuntu:jammy
LABEL org.opencontainers.image.source=https://github.com/fish-shell/fish-shell
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV CXXFLAGS="-Werror=address -Werror=return-type -Wno-psabi"
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get -y install \
build-essential \
cmake \
file \
g++ \
gettext \
git \
libncurses5-dev \
libpcre2-dev \
locales \
ninja-build \
pkg-config \
python3 \
python3-pexpect \
sudo \
&& locale-gen en_US.UTF-8 \
&& apt-get clean
RUN groupadd -g 1000 fishuser \
&& useradd -p $(openssl passwd -1 fish) -d /home/fishuser -m -u 1000 -g 1000 fishuser \
&& adduser fishuser sudo \
&& mkdir -p /home/fishuser/fish-build \
&& mkdir /fish-source \
&& chown -R fishuser:fishuser /home/fishuser /fish-source
USER fishuser
WORKDIR /home/fishuser
COPY fish_run_tests.sh /
CMD /fish_run_tests.sh

1073
fish-rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +0,0 @@
[package]
name = "fish-rust"
version = "0.1.0"
edition = "2021"
rust-version = "1.67"
[dependencies]
widestring-suffix = { path = "./widestring-suffix/" }
autocxx = "0.23.1"
bitflags = "1.3.2"
cxx = "1.0"
errno = "0.2.8"
inventory = { version = "0.3.3", optional = true}
libc = "0.2.137"
nix = { version = "0.25.0", default-features = false, features = [] }
num-traits = "0.2.15"
once_cell = "1.17.0"
rand = { version = "0.8.5", features = ["small_rng"] }
unixstring = "0.2.7"
widestring = "1.0.2"
[build-dependencies]
autocxx-build = "0.23.1"
cxx-build = { git = "https://github.com/fish-shell/cxx", branch = "fish" }
cxx-gen = { git = "https://github.com/fish-shell/cxx", branch = "fish" }
miette = { version = "5", features = ["fancy"] }
[lib]
crate-type=["staticlib"]
[features]
# The fish-ffi-tests feature causes tests to be built which need to use the FFI.
# These tests are run by fish_tests().
default = ["fish-ffi-tests"]
fish-ffi-tests = ["inventory"]
[patch.crates-io]
cc = { git = "https://github.com/mqudsi/cc-rs", branch = "fish" }
cxx = { git = "https://github.com/fish-shell/cxx", branch = "fish" }
cxx-gen = { git = "https://github.com/fish-shell/cxx", branch = "fish" }
autocxx = { git = "https://github.com/fish-shell/autocxx", branch = "fish" }
autocxx-build = { git = "https://github.com/fish-shell/autocxx", branch = "fish" }
autocxx-bindgen = { git = "https://github.com/fish-shell/autocxx-bindgen", branch = "fish" }
[patch.'https://github.com/fish-shell/cxx']
cc = { git = "https://github.com/mqudsi/cc-rs", branch = "fish" }
[patch.'https://github.com/fish-shell/autocxx']
cc = { git = "https://github.com/mqudsi/cc-rs", branch = "fish" }
#cxx = { path = "../../cxx" }
#cxx-gen = { path="../../cxx/gen/lib" }
#autocxx = { path = "../../autocxx" }
#autocxx-build = { path = "../../autocxx/gen/build" }
#autocxx-bindgen = { path = "../../autocxx-bindgen" }
[profile.release]
overflow-checks = true

View File

@ -1,69 +0,0 @@
fn main() -> miette::Result<()> {
let rust_dir = std::env::var("CARGO_MANIFEST_DIR").expect("Env var CARGO_MANIFEST_DIR missing");
let target_dir =
std::env::var("FISH_RUST_TARGET_DIR").unwrap_or(format!("{}/{}", rust_dir, "target/"));
let fish_src_dir = format!("{}/{}", rust_dir, "../src/");
// Where cxx emits its header.
let cxx_include_dir = format!("{}/{}", target_dir, "cxxbridge/rust/");
// If FISH_BUILD_DIR is given by CMake, then use it; otherwise assume it's at ../build.
let fish_build_dir =
std::env::var("FISH_BUILD_DIR").unwrap_or(format!("{}/{}", rust_dir, "../build/"));
// Where autocxx should put its stuff.
let autocxx_gen_dir = std::env::var("FISH_AUTOCXX_GEN_DIR")
.unwrap_or(format!("{}/{}", fish_build_dir, "fish-autocxx-gen/"));
// Emit cxx junk.
// This allows "Rust to be used from C++"
// This must come before autocxx so that cxx can emit its cxx.h header.
let source_files = vec![
"src/abbrs.rs",
"src/event.rs",
"src/fd_monitor.rs",
"src/fd_readable_set.rs",
"src/fds.rs",
"src/ffi_init.rs",
"src/ffi_tests.rs",
"src/future_feature_flags.rs",
"src/job_group.rs",
"src/parse_constants.rs",
"src/redirection.rs",
"src/smoke.rs",
"src/timer.rs",
"src/tokenizer.rs",
"src/topic_monitor.rs",
"src/util.rs",
"src/builtins/shared.rs",
];
cxx_build::bridges(&source_files)
.flag_if_supported("-std=c++11")
.include(&fish_src_dir)
.include(&fish_build_dir) // For config.h
.include(&cxx_include_dir) // For cxx.h
.flag("-Wno-comment")
.compile("fish-rust");
// Emit autocxx junk.
// This allows "C++ to be used from Rust."
let include_paths = [&fish_src_dir, &fish_build_dir, &cxx_include_dir];
let mut builder = autocxx_build::Builder::new("src/ffi.rs", include_paths);
// Use autocxx's custom output directory unless we're being called by `rust-analyzer` and co.,
// in which case stick to the default target directory so code intelligence continues to work.
if std::env::var("RUSTC_WRAPPER").map_or(true, |wrapper| {
!(wrapper.contains("rust-analyzer") || wrapper.contains("intellij-rust-native-helper"))
}) {
// We need this reassignment because of how the builder pattern works
builder = builder.custom_gendir(autocxx_gen_dir.into());
}
let mut b = builder.build()?;
b.flag_if_supported("-std=c++11")
.flag("-Wno-comment")
.compile("fish-rust-autocxx");
for file in source_files {
println!("cargo:rerun-if-changed={file}");
}
Ok(())
}

View File

@ -1,470 +0,0 @@
#![allow(clippy::extra_unused_lifetimes, clippy::needless_lifetimes)]
use std::{
collections::HashSet,
sync::{Arc, Mutex, MutexGuard},
};
use crate::wchar::{wstr, WString};
use crate::{
wchar::L,
wchar_ffi::{WCharFromFFI, WCharToFFI},
};
use cxx::{CxxWString, UniquePtr};
use once_cell::sync::Lazy;
use crate::abbrs::abbrs_ffi::abbrs_replacer_t;
use crate::ffi::re::regex_t;
use crate::parse_constants::SourceRange;
use self::abbrs_ffi::{abbreviation_t, abbrs_position_t, abbrs_replacement_t};
#[cxx::bridge]
mod abbrs_ffi {
extern "C++" {
include!("re.h");
include!("parse_constants.h");
type SourceRange = crate::parse_constants::SourceRange;
}
enum abbrs_position_t {
command,
anywhere,
}
struct abbrs_replacer_t {
replacement: UniquePtr<CxxWString>,
is_function: bool,
set_cursor_marker: UniquePtr<CxxWString>,
has_cursor_marker: bool,
}
struct abbrs_replacement_t {
range: SourceRange,
text: UniquePtr<CxxWString>,
cursor: usize,
has_cursor: bool,
}
struct abbreviation_t {
key: UniquePtr<CxxWString>,
replacement: UniquePtr<CxxWString>,
is_regex: bool,
}
extern "Rust" {
type GlobalAbbrs<'a>;
#[cxx_name = "abbrs_list"]
fn abbrs_list_ffi() -> Vec<abbreviation_t>;
#[cxx_name = "abbrs_match"]
fn abbrs_match_ffi(token: &CxxWString, position: abbrs_position_t)
-> Vec<abbrs_replacer_t>;
#[cxx_name = "abbrs_has_match"]
fn abbrs_has_match_ffi(token: &CxxWString, position: abbrs_position_t) -> bool;
#[cxx_name = "abbrs_replacement_from"]
fn abbrs_replacement_from_ffi(
range: SourceRange,
text: &CxxWString,
set_cursor_marker: &CxxWString,
has_cursor_marker: bool,
) -> abbrs_replacement_t;
#[cxx_name = "abbrs_get_set"]
unsafe fn abbrs_get_set_ffi<'a>() -> Box<GlobalAbbrs<'a>>;
unsafe fn add<'a>(
self: &mut GlobalAbbrs<'_>,
name: &CxxWString,
key: &CxxWString,
replacement: &CxxWString,
position: abbrs_position_t,
from_universal: bool,
);
unsafe fn erase<'a>(self: &mut GlobalAbbrs<'_>, name: &CxxWString);
}
}
static abbrs: Lazy<Arc<Mutex<AbbreviationSet>>> =
Lazy::new(|| Arc::new(Mutex::new(Default::default())));
pub fn with_abbrs<R>(cb: impl FnOnce(&AbbreviationSet) -> R) -> R {
let abbrs_g = abbrs.lock().unwrap();
cb(&abbrs_g)
}
pub fn with_abbrs_mut<R>(cb: impl FnOnce(&mut AbbreviationSet) -> R) -> R {
let mut abbrs_g = abbrs.lock().unwrap();
cb(&mut abbrs_g)
}
/// Controls where in the command line abbreviations may expand.
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Position {
Command, // expand in command position
Anywhere, // expand in any token
}
impl From<abbrs_position_t> for Position {
fn from(value: abbrs_position_t) -> Self {
match value {
abbrs_position_t::anywhere => Position::Anywhere,
abbrs_position_t::command => Position::Command,
_ => panic!("invalid abbrs_position_t"),
}
}
}
#[derive(Debug)]
pub struct Abbreviation {
// Abbreviation name. This is unique within the abbreviation set.
// This is used as the token to match unless we have a regex.
pub name: WString,
/// The key (recognized token) - either a literal or a regex pattern.
pub key: WString,
/// If set, use this regex to recognize tokens.
/// If unset, the key is to be interpreted literally.
/// Note that the fish interface enforces that regexes match the entire token;
/// we accomplish this by surrounding the regex in ^ and $.
pub regex: Option<UniquePtr<regex_t>>,
/// Replacement string.
pub replacement: WString,
/// If set, the replacement is a function name.
pub replacement_is_function: bool,
/// Expansion position.
pub position: Position,
/// If set, then move the cursor to the first instance of this string in the expansion.
pub set_cursor_marker: Option<WString>,
/// Mark if we came from a universal variable.
pub from_universal: bool,
}
impl Abbreviation {
// Construct from a name, a key which matches a token, a replacement token, a position, and
// whether we are derived from a universal variable.
pub fn new(
name: WString,
key: WString,
replacement: WString,
position: Position,
from_universal: bool,
) -> Self {
Self {
name,
key,
regex: None,
replacement,
replacement_is_function: false,
position,
set_cursor_marker: None,
from_universal,
}
}
// \return true if this is a regex abbreviation.
pub fn is_regex(&self) -> bool {
self.regex.is_some()
}
// \return true if we match a token at a given position.
pub fn matches(&self, token: &wstr, position: Position) -> bool {
if !self.matches_position(position) {
return false;
}
self.regex
.as_ref()
.map(|r| r.matches_ffi(&token.to_ffi()))
.unwrap_or(self.key == token)
}
// \return if we expand in a given position.
fn matches_position(&self, position: Position) -> bool {
return self.position == Position::Anywhere || self.position == position;
}
}
/// The result of an abbreviation expansion.
pub struct Replacer {
/// The string to use to replace the incoming token, either literal or as a function name.
replacement: WString,
/// If true, treat 'replacement' as the name of a function.
is_function: bool,
/// If set, the cursor should be moved to the first instance of this string in the expansion.
set_cursor_marker: Option<WString>,
}
impl From<Replacer> for abbrs_replacer_t {
fn from(value: Replacer) -> Self {
let has_cursor_marker = value.set_cursor_marker.is_some();
Self {
replacement: value.replacement.to_ffi(),
is_function: value.is_function,
set_cursor_marker: value.set_cursor_marker.unwrap_or_default().to_ffi(),
has_cursor_marker,
}
}
}
struct Replacement {
/// The original range of the token in the command line.
range: SourceRange,
/// The string to replace with.
text: WString,
/// The new cursor location, or none to use the default.
/// This is relative to the original range.
cursor: Option<usize>,
}
impl Replacement {
/// Construct a replacement from a replacer.
/// The \p range is the range of the text matched by the replacer in the command line.
/// The text is passed in separately as it may be the output of the replacer's function.
fn from(range: SourceRange, mut text: WString, set_cursor_marker: Option<WString>) -> Self {
let mut cursor = None;
if let Some(set_cursor_marker) = set_cursor_marker {
let matched = text
.as_char_slice()
.windows(set_cursor_marker.len())
.position(|w| w == set_cursor_marker.as_char_slice());
if let Some(start) = matched {
text.replace_range(start..(start + set_cursor_marker.len()), L!(""));
cursor = Some(start + range.start as usize)
}
}
Self {
range,
text,
cursor,
}
}
}
#[derive(Default)]
pub struct AbbreviationSet {
/// List of abbreviations, in definition order.
abbrs: Vec<Abbreviation>,
/// Set of used abbrevation names.
/// This is to avoid a linear scan when adding new abbreviations.
used_names: HashSet<WString>,
}
impl AbbreviationSet {
/// \return the list of replacers for an input token, in priority order.
/// The \p position is given to describe where the token was found.
pub fn r#match(&self, token: &wstr, position: Position) -> Vec<Replacer> {
let mut result = vec![];
// Later abbreviations take precedence so walk backwards.
for abbr in self.abbrs.iter().rev() {
if abbr.matches(token, position) {
result.push(Replacer {
replacement: abbr.replacement.clone(),
is_function: abbr.replacement_is_function,
set_cursor_marker: abbr.set_cursor_marker.clone(),
});
}
}
return result;
}
/// \return whether we would have at least one replacer for a given token.
pub fn has_match(&self, token: &wstr, position: Position) -> bool {
self.abbrs.iter().any(|abbr| abbr.matches(token, position))
}
/// Add an abbreviation. Any abbreviation with the same name is replaced.
pub fn add(&mut self, abbr: Abbreviation) {
assert!(!abbr.name.is_empty(), "Invalid name");
let inserted = self.used_names.insert(abbr.name.clone());
if !inserted {
// Name was already used, do a linear scan to find it.
let index = self
.abbrs
.iter()
.position(|a| a.name == abbr.name)
.expect("Abbreviation not found though its name was present");
self.abbrs.remove(index);
}
self.abbrs.push(abbr);
}
/// Rename an abbreviation. This asserts that the old name is used, and the new name is not; the
/// caller should check these beforehand with has_name().
pub fn rename(&mut self, old_name: &wstr, new_name: &wstr) {
let erased = self.used_names.remove(old_name);
let inserted = self.used_names.insert(new_name.to_owned());
assert!(
erased && inserted,
"Old name not found or new name already present"
);
for abbr in self.abbrs.iter_mut() {
if abbr.name == old_name {
abbr.name = new_name.to_owned();
break;
}
}
}
/// Erase an abbreviation by name.
/// \return true if erased, false if not found.
pub fn erase(&mut self, name: &wstr) -> bool {
let erased = self.used_names.remove(name);
if !erased {
return false;
}
for (index, abbr) in self.abbrs.iter().enumerate().rev() {
if abbr.name == name {
self.abbrs.remove(index);
return true;
}
}
panic!("Unable to find named abbreviation");
}
/// \return true if we have an abbreviation with the given name.
pub fn has_name(&self, name: &wstr) -> bool {
self.used_names.contains(name)
}
/// \return a reference to the abbreviation list.
pub fn list(&self) -> &[Abbreviation] {
&self.abbrs
}
}
/// \return the list of replacers for an input token, in priority order, using the global set.
/// The \p position is given to describe where the token was found.
fn abbrs_match_ffi(token: &CxxWString, position: abbrs_position_t) -> Vec<abbrs_replacer_t> {
with_abbrs(|set| set.r#match(&token.from_ffi(), position.into()))
.into_iter()
.map(|r| r.into())
.collect()
}
fn abbrs_has_match_ffi(token: &CxxWString, position: abbrs_position_t) -> bool {
with_abbrs(|set| set.has_match(&token.from_ffi(), position.into()))
}
fn abbrs_list_ffi() -> Vec<abbreviation_t> {
with_abbrs(|set| -> Vec<abbreviation_t> {
let list = set.list();
let mut result = Vec::with_capacity(list.len());
for abbr in list {
result.push(abbreviation_t {
key: abbr.key.to_ffi(),
replacement: abbr.replacement.to_ffi(),
is_regex: abbr.is_regex(),
})
}
result
})
}
fn abbrs_get_set_ffi<'a>() -> Box<GlobalAbbrs<'a>> {
let abbrs_g = abbrs.lock().unwrap();
Box::new(GlobalAbbrs { g: abbrs_g })
}
fn abbrs_replacement_from_ffi(
range: SourceRange,
text: &CxxWString,
set_cursor_marker: &CxxWString,
has_cursor_marker: bool,
) -> abbrs_replacement_t {
let cursor_marker = if has_cursor_marker {
Some(set_cursor_marker.from_ffi())
} else {
None
};
let replacement = Replacement::from(range, text.from_ffi(), cursor_marker);
abbrs_replacement_t {
range,
text: replacement.text.to_ffi(),
cursor: replacement.cursor.unwrap_or_default(),
has_cursor: replacement.cursor.is_some(),
}
}
pub struct GlobalAbbrs<'a> {
g: MutexGuard<'a, AbbreviationSet>,
}
impl<'a> GlobalAbbrs<'a> {
fn add(
&mut self,
name: &CxxWString,
key: &CxxWString,
replacement: &CxxWString,
position: abbrs_position_t,
from_universal: bool,
) {
self.g.add(Abbreviation::new(
name.from_ffi(),
key.from_ffi(),
replacement.from_ffi(),
position.into(),
from_universal,
));
}
fn erase(&mut self, name: &CxxWString) {
self.g.erase(&name.from_ffi());
}
}
use crate::ffi_tests::add_test;
add_test!("rename_abbrs", || {
use crate::wchar::wstr;
use crate::{
abbrs::{Abbreviation, Position},
wchar::L,
};
with_abbrs_mut(|abbrs_g| {
let mut add = |name: &wstr, repl: &wstr, position: Position| {
abbrs_g.add(Abbreviation {
name: name.into(),
key: name.into(),
regex: None,
replacement: repl.into(),
replacement_is_function: false,
position,
set_cursor_marker: None,
from_universal: false,
})
};
add(L!("gc"), L!("git checkout"), Position::Command);
add(L!("foo"), L!("bar"), Position::Command);
add(L!("gx"), L!("git checkout"), Position::Command);
add(L!("yin"), L!("yang"), Position::Anywhere);
assert!(!abbrs_g.has_name(L!("gcc")));
assert!(abbrs_g.has_name(L!("gc")));
abbrs_g.rename(L!("gc"), L!("gcc"));
assert!(abbrs_g.has_name(L!("gcc")));
assert!(!abbrs_g.has_name(L!("gc")));
assert!(!abbrs_g.erase(L!("gc")));
assert!(abbrs_g.erase(L!("gcc")));
assert!(!abbrs_g.erase(L!("gcc")));
})
});

View File

@ -1,604 +0,0 @@
use crate::abbrs::{self, Abbreviation, Position};
use crate::builtins::shared::{
builtin_missing_argument, builtin_print_error_trailer, builtin_print_help,
builtin_unknown_option, io_streams_t, BUILTIN_ERR_TOO_MANY_ARGUMENTS, STATUS_CMD_ERROR,
STATUS_CMD_OK, STATUS_INVALID_ARGS,
};
use crate::common::{escape_string, valid_func_name, EscapeStringStyle};
use crate::env::flags::EnvMode;
use crate::env::status::{ENV_NOT_FOUND, ENV_OK};
use crate::ffi::{self, parser_t};
use crate::re::regex_make_anchored;
use crate::wchar::{wstr, L};
use crate::wchar_ffi::WCharFromFFI;
use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t};
use crate::wutil::wgettext_fmt;
use libc::c_int;
pub use widestring::Utf32String as WString;
const CMD: &wstr = L!("abbr");
#[derive(Default, Debug)]
struct Options {
add: bool,
rename: bool,
show: bool,
list: bool,
erase: bool,
query: bool,
function: Option<WString>,
regex_pattern: Option<WString>,
position: Option<Position>,
set_cursor_marker: Option<WString>,
args: Vec<WString>,
}
impl Options {
fn validate(&mut self, streams: &mut io_streams_t) -> bool {
// Duplicate options?
let mut cmds = vec![];
if self.add {
cmds.push(L!("add"))
};
if self.rename {
cmds.push(L!("rename"))
};
if self.show {
cmds.push(L!("show"))
};
if self.list {
cmds.push(L!("list"))
};
if self.erase {
cmds.push(L!("erase"))
};
if self.query {
cmds.push(L!("query"))
};
if cmds.len() > 1 {
streams.err.append(wgettext_fmt!(
"%ls: Cannot combine options %ls\n",
CMD,
join(&cmds, L!(", "))
));
return false;
}
// If run with no options, treat it like --add if we have arguments,
// or --show if we do not have any arguments.
if cmds.is_empty() {
self.show = self.args.is_empty();
self.add = !self.args.is_empty();
}
if !self.add && self.position.is_some() {
streams.err.append(wgettext_fmt!(
"%ls: --position option requires --add\n",
CMD
));
return false;
}
if !self.add && self.regex_pattern.is_some() {
streams
.err
.append(wgettext_fmt!("%ls: --regex option requires --add\n", CMD));
return false;
}
if !self.add && self.function.is_some() {
streams.err.append(wgettext_fmt!(
"%ls: --function option requires --add\n",
CMD
));
return false;
}
if !self.add && self.set_cursor_marker.is_some() {
streams.err.append(wgettext_fmt!(
"%ls: --set-cursor option requires --add\n",
CMD
));
return false;
}
if self
.set_cursor_marker
.as_ref()
.map(|m| m.is_empty())
.unwrap_or(false)
{
streams.err.append(wgettext_fmt!(
"%ls: --set-cursor argument cannot be empty\n",
CMD
));
return false;
}
return true;
}
}
fn join(list: &[&wstr], sep: &wstr) -> WString {
let mut result = WString::new();
let mut iter = list.iter();
let first = match iter.next() {
Some(first) => first,
None => return result,
};
result.push_utfstr(first);
for s in iter {
result.push_utfstr(sep);
result.push_utfstr(s);
}
result
}
// Print abbreviations in a fish-script friendly way.
fn abbr_show(streams: &mut io_streams_t) -> Option<c_int> {
let style = EscapeStringStyle::Script(Default::default());
abbrs::with_abbrs(|abbrs| {
let mut result = WString::new();
for abbr in abbrs.list() {
result.clear();
let mut add_arg = |arg: &wstr| {
if !result.is_empty() {
result.push_str(" ");
}
result.push_utfstr(arg);
};
add_arg(L!("abbr -a"));
if abbr.is_regex() {
add_arg(L!("--regex"));
add_arg(&escape_string(&abbr.key, style));
}
if abbr.position != Position::Command {
add_arg(L!("--position"));
add_arg(L!("anywhere"));
}
if let Some(ref set_cursor_marker) = abbr.set_cursor_marker {
add_arg(L!("--set-cursor="));
add_arg(&escape_string(set_cursor_marker, style));
}
if abbr.replacement_is_function {
add_arg(L!("--function"));
add_arg(&escape_string(&abbr.replacement, style));
}
add_arg(L!("--"));
// Literal abbreviations have the name and key as the same.
// Regex abbreviations have a pattern separate from the name.
add_arg(&escape_string(&abbr.name, style));
if !abbr.replacement_is_function {
add_arg(&escape_string(&abbr.replacement, style));
}
if abbr.from_universal {
add_arg(L!("# imported from a universal variable, see `help abbr`"));
}
result.push('\n');
streams.out.append(&result);
}
});
return STATUS_CMD_OK;
}
// Print the list of abbreviation names.
fn abbr_list(opts: &Options, streams: &mut io_streams_t) -> Option<c_int> {
const subcmd: &wstr = L!("--list");
if !opts.args.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls %ls: Unexpected argument -- '%ls'\n",
CMD,
subcmd,
opts.args[0]
));
return STATUS_INVALID_ARGS;
}
abbrs::with_abbrs(|abbrs| {
for abbr in abbrs.list() {
let mut name = abbr.name.clone();
name.push('\n');
streams.out.append(name);
}
});
return STATUS_CMD_OK;
}
// Rename an abbreviation, deleting any existing one with the given name.
fn abbr_rename(opts: &Options, streams: &mut io_streams_t) -> Option<c_int> {
const subcmd: &wstr = L!("--rename");
if opts.args.len() != 2 {
streams.err.append(wgettext_fmt!(
"%ls %ls: Requires exactly two arguments\n",
CMD,
subcmd
));
return STATUS_INVALID_ARGS;
}
let old_name = &opts.args[0];
let new_name = &opts.args[1];
if old_name.is_empty() || new_name.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls %ls: Name cannot be empty\n",
CMD,
subcmd
));
return STATUS_INVALID_ARGS;
}
if contains_whitespace(new_name) {
streams.err.append(wgettext_fmt!(
"%ls %ls: Abbreviation '%ls' cannot have spaces in the word\n",
CMD,
subcmd,
new_name.as_utfstr()
));
return STATUS_INVALID_ARGS;
}
abbrs::with_abbrs_mut(|abbrs| -> Option<c_int> {
if !abbrs.has_name(old_name) {
streams.err.append(wgettext_fmt!(
"%ls %ls: No abbreviation named %ls\n",
CMD,
subcmd,
old_name.as_utfstr()
));
return STATUS_CMD_ERROR;
}
if abbrs.has_name(new_name) {
streams.err.append(wgettext_fmt!(
"%ls %ls: Abbreviation %ls already exists, cannot rename %ls\n",
CMD,
subcmd,
new_name.as_utfstr(),
old_name.as_utfstr()
));
return STATUS_INVALID_ARGS;
}
abbrs.rename(old_name, new_name);
STATUS_CMD_OK
})
}
fn contains_whitespace(val: &wstr) -> bool {
val.chars().any(char::is_whitespace)
}
// Test if any args is an abbreviation.
fn abbr_query(opts: &Options) -> Option<c_int> {
// Return success if any of our args matches an abbreviation.
abbrs::with_abbrs(|abbrs| {
for arg in opts.args.iter() {
if abbrs.has_name(arg) {
return STATUS_CMD_OK;
}
}
return STATUS_CMD_ERROR;
})
}
// Add a named abbreviation.
fn abbr_add(opts: &Options, streams: &mut io_streams_t) -> Option<c_int> {
const subcmd: &wstr = L!("--add");
if opts.args.len() < 2 && opts.function.is_none() {
streams.err.append(wgettext_fmt!(
"%ls %ls: Requires at least two arguments\n",
CMD,
subcmd
));
return STATUS_INVALID_ARGS;
}
if opts.args.is_empty() || opts.args[0].is_empty() {
streams.err.append(wgettext_fmt!(
"%ls %ls: Name cannot be empty\n",
CMD,
subcmd
));
return STATUS_INVALID_ARGS;
}
let name = &opts.args[0];
if name.chars().any(|c| c.is_whitespace()) {
streams.err.append(wgettext_fmt!(
"%ls %ls: Abbreviation '%ls' cannot have spaces in the word\n",
CMD,
subcmd,
name.as_utfstr()
));
return STATUS_INVALID_ARGS;
}
let mut regex = None;
let key = if let Some(ref regex_pattern) = opts.regex_pattern {
// Compile the regex as given; if that succeeds then wrap it in our ^$ so it matches the
// entire token.
let flags = ffi::re::flags_t { icase: false };
let result = ffi::try_compile(regex_pattern, &flags);
if result.has_error() {
let error = result.get_error();
streams.err.append(wgettext_fmt!(
"%ls: Regular expression compile error: %ls\n",
CMD,
&error.message().from_ffi()
));
streams
.err
.append(wgettext_fmt!("%ls: %ls\n", CMD, regex_pattern.as_utfstr()));
streams
.err
.append(wgettext_fmt!("%ls: %*ls\n", CMD, error.offset, "^"));
return STATUS_INVALID_ARGS;
}
let anchored = regex_make_anchored(regex_pattern);
let mut result = ffi::try_compile(&anchored, &flags);
assert!(
!result.has_error(),
"Anchored compilation should have succeeded"
);
let re = result.as_mut().get_regex();
assert!(!re.is_null(), "Anchored compilation should have succeeded");
let _ = regex.insert(re);
regex_pattern
} else {
// The name plays double-duty as the token to replace.
name
};
if opts.function.is_some() && opts.args.len() > 1 {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, L!("abbr")));
return STATUS_INVALID_ARGS;
}
let replacement = if let Some(ref function) = opts.function {
// Abbreviation function names disallow spaces.
// This is to prevent accidental usage of e.g. `--function 'string replace'`
if !valid_func_name(function) || contains_whitespace(function) {
streams.err.append(wgettext_fmt!(
"%ls: Invalid function name: %ls\n",
CMD,
function.as_utfstr()
));
return STATUS_INVALID_ARGS;
}
function.clone()
} else {
let mut replacement = WString::new();
for iter in opts.args.iter().skip(1) {
if !replacement.is_empty() {
replacement.push(' ')
};
replacement.push_utfstr(iter);
}
replacement
};
let position = opts.position.unwrap_or(Position::Command);
// Note historically we have allowed overwriting existing abbreviations.
abbrs::with_abbrs_mut(move |abbrs| {
abbrs.add(Abbreviation {
name: name.clone(),
key: key.clone(),
regex,
replacement,
replacement_is_function: opts.function.is_some(),
position,
set_cursor_marker: opts.set_cursor_marker.clone(),
from_universal: false,
})
});
return STATUS_CMD_OK;
}
// Erase the named abbreviations.
fn abbr_erase(opts: &Options, parser: &mut parser_t) -> Option<c_int> {
if opts.args.is_empty() {
// This has historically been a silent failure.
return STATUS_CMD_ERROR;
}
// Erase each. If any is not found, return ENV_NOT_FOUND which is historical.
abbrs::with_abbrs_mut(|abbrs| -> Option<c_int> {
let mut result = STATUS_CMD_OK;
for arg in &opts.args {
if !abbrs.erase(arg) {
result = Some(ENV_NOT_FOUND);
}
// Erase the old uvar - this makes `abbr -e` work.
let esc_src = escape_string(arg, EscapeStringStyle::Script(Default::default()));
if !esc_src.is_empty() {
let var_name = WString::from_str("_fish_abbr_") + esc_src.as_utfstr();
let ret = parser.remove_var(&var_name, EnvMode::UNIVERSAL.into());
if ret == autocxx::c_int(ENV_OK) {
result = STATUS_CMD_OK
};
}
}
result
})
}
pub fn abbr(
parser: &mut parser_t,
streams: &mut io_streams_t,
argv: &mut [&wstr],
) -> Option<c_int> {
let mut argv_read = Vec::with_capacity(argv.len());
argv_read.extend_from_slice(argv);
let cmd = argv[0];
// Note 1 is returned by wgetopt to indicate a non-option argument.
const NON_OPTION_ARGUMENT: char = 1 as char;
const SET_CURSOR_SHORT: char = 2 as char;
const RENAME_SHORT: char = 3 as char;
// Note the leading '-' causes wgetopter to return arguments in order, instead of permuting
// them. We need this behavior for compatibility with pre-builtin abbreviations where options
// could be given literally, for example `abbr e emacs -nw`.
const short_options: &wstr = L!("-:af:r:seqgUh");
const longopts: &[woption] = &[
wopt(L!("add"), woption_argument_t::no_argument, 'a'),
wopt(L!("position"), woption_argument_t::required_argument, 'p'),
wopt(L!("regex"), woption_argument_t::required_argument, 'r'),
wopt(
L!("set-cursor"),
woption_argument_t::optional_argument,
SET_CURSOR_SHORT,
),
wopt(L!("function"), woption_argument_t::required_argument, 'f'),
wopt(L!("rename"), woption_argument_t::no_argument, RENAME_SHORT),
wopt(L!("erase"), woption_argument_t::no_argument, 'e'),
wopt(L!("query"), woption_argument_t::no_argument, 'q'),
wopt(L!("show"), woption_argument_t::no_argument, 's'),
wopt(L!("list"), woption_argument_t::no_argument, 'l'),
wopt(L!("global"), woption_argument_t::no_argument, 'g'),
wopt(L!("universal"), woption_argument_t::no_argument, 'U'),
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
];
let mut opts = Options::default();
let mut w = wgetopter_t::new(short_options, longopts, argv);
while let Some(c) = w.wgetopt_long() {
match c {
NON_OPTION_ARGUMENT => {
// If --add is specified (or implied by specifying no other commands), all
// unrecognized options after the *second* non-option argument are considered part
// of the abbreviation expansion itself, rather than options to the abbr command.
// For example, `abbr e emacs -nw` works, because `-nw` occurs after the second
// non-option, and --add is implied.
if let Some(arg) = w.woptarg {
opts.args.push(arg.to_owned())
};
if opts.args.len() >= 2
&& !(opts.rename || opts.show || opts.list || opts.erase || opts.query)
{
break;
}
}
'a' => opts.add = true,
'p' => {
if opts.position.is_some() {
streams.err.append(wgettext_fmt!(
"%ls: Cannot specify multiple positions\n",
CMD
));
return STATUS_INVALID_ARGS;
}
if w.woptarg == Some(L!("command")) {
opts.position = Some(Position::Command);
} else if w.woptarg == Some(L!("anywhere")) {
opts.position = Some(Position::Anywhere);
} else {
streams.err.append(wgettext_fmt!(
"%ls: Invalid position '%ls'\n",
CMD,
w.woptarg.unwrap_or_default()
));
streams
.err
.append(L!("Position must be one of: command, anywhere.\n"));
return STATUS_INVALID_ARGS;
}
}
'r' => {
if opts.regex_pattern.is_some() {
streams.err.append(wgettext_fmt!(
"%ls: Cannot specify multiple regex patterns\n",
CMD
));
return STATUS_INVALID_ARGS;
}
opts.regex_pattern = w.woptarg.map(ToOwned::to_owned);
}
SET_CURSOR_SHORT => {
if opts.set_cursor_marker.is_some() {
streams.err.append(wgettext_fmt!(
"%ls: Cannot specify multiple set-cursor options\n",
CMD
));
return STATUS_INVALID_ARGS;
}
// The default set-cursor indicator is '%'.
let _ = opts
.set_cursor_marker
.insert(w.woptarg.unwrap_or(L!("%")).to_owned());
}
'f' => opts.function = w.woptarg.map(ToOwned::to_owned),
RENAME_SHORT => opts.rename = true,
'e' => opts.erase = true,
'q' => opts.query = true,
's' => opts.show = true,
'l' => opts.list = true,
// Kept for backwards compatibility but ignored.
// This basically does nothing now.
'g' => {}
'U' => {
// Kept and made ineffective, so we warn.
streams.err.append(wgettext_fmt!(
"%ls: Warning: Option '%ls' was removed and is now ignored",
cmd,
argv_read[w.woptind - 1]
));
builtin_print_error_trailer(parser, streams, cmd);
}
'h' => {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true);
return STATUS_INVALID_ARGS;
}
'?' => {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], false);
return STATUS_INVALID_ARGS;
}
_ => {
panic!("unexpected retval from wgeopter.next()");
}
}
}
for arg in argv_read[w.woptind..].iter() {
opts.args.push((*arg).into());
}
if !opts.validate(streams) {
return STATUS_INVALID_ARGS;
}
if opts.add {
return abbr_add(&opts, streams);
};
if opts.show {
return abbr_show(streams);
};
if opts.list {
return abbr_list(&opts, streams);
};
if opts.rename {
return abbr_rename(&opts, streams);
};
if opts.erase {
return abbr_erase(&opts, parser);
};
if opts.query {
return abbr_query(&opts);
};
// validate() should error or ensure at least one path is set.
panic!("unreachable");
}

View File

@ -1,139 +0,0 @@
// Implementation of the bg builtin.
use std::pin::Pin;
use super::shared::{builtin_print_help, io_streams_t, STATUS_CMD_ERROR, STATUS_INVALID_ARGS};
use crate::{
builtins::shared::{HelpOnlyCmdOpts, STATUS_CMD_OK},
ffi::{self, parser_t, Repin},
wchar::wstr,
wchar_ffi::{c_str, WCharFromFFI, WCharToFFI},
wutil::{fish_wcstoi, wgettext_fmt},
};
use libc::c_int;
/// Helper function for builtin_bg().
fn send_to_bg(
parser: &mut parser_t,
streams: &mut io_streams_t,
cmd: &wstr,
job_pos: usize,
) -> Option<c_int> {
let job = parser.get_jobs()[job_pos]
.as_ref()
.expect("job_pos must be valid");
if !job.wants_job_control() {
let err = wgettext_fmt!(
"%ls: Can't put job %d, '%ls' to background because it is not under job control\n",
cmd,
job.job_id().0,
job.command().from_ffi()
);
ffi::builtin_print_help(
parser.pin(),
streams.ffi_ref(),
c_str!(cmd),
err.to_ffi().as_ref()?,
);
return STATUS_CMD_ERROR;
}
streams.err.append(wgettext_fmt!(
"Send job %d '%ls' to background\n",
job.job_id().0,
job.command().from_ffi()
));
unsafe {
std::mem::transmute::<&ffi::job_group_t, &crate::job_group::JobGroup>(job.ffi_group())
}
.set_is_foreground(false);
if !job.ffi_resume() {
return STATUS_CMD_ERROR;
}
parser.pin().job_promote_at(job_pos);
return STATUS_CMD_OK;
}
/// Builtin for putting a job in the background.
pub fn bg(parser: &mut parser_t, streams: &mut io_streams_t, args: &mut [&wstr]) -> Option<c_int> {
let opts = match HelpOnlyCmdOpts::parse(args, parser, streams) {
Ok(opts) => opts,
Err(err @ Some(_)) if err != STATUS_CMD_OK => return err,
Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"),
};
let cmd = args[0];
if opts.print_help {
builtin_print_help(parser, streams, args.get(0)?);
return STATUS_CMD_OK;
}
if opts.optind == args.len() {
// No jobs were specified so use the most recent (i.e., last) job.
let jobs = parser.get_jobs();
let job_pos = jobs.iter().position(|job| {
if let Some(job) = job.as_ref() {
return job.is_stopped() && job.wants_job_control() && !job.is_completed();
}
false
});
let Some(job_pos) = job_pos else {
streams
.err
.append(wgettext_fmt!("%ls: There are no suitable jobs\n", cmd));
return STATUS_CMD_ERROR;
};
return send_to_bg(parser, streams, cmd, job_pos);
}
// The user specified at least one job to be backgrounded.
// If one argument is not a valid pid (i.e. integer >= 0), fail without backgrounding anything,
// but still print errors for all of them.
let mut retval = STATUS_CMD_OK;
let pids: Vec<i64> = args[opts.optind..]
.iter()
.map(|arg| {
fish_wcstoi(arg.chars()).unwrap_or_else(|_| {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is not a valid job specifier\n",
cmd,
*arg
));
retval = STATUS_INVALID_ARGS;
0
})
})
.collect();
if retval != STATUS_CMD_OK {
return retval;
}
// Background all existing jobs that match the pids.
// Non-existent jobs aren't an error, but information about them is useful.
for pid in pids {
let mut job_pos = 0;
let job = unsafe {
parser
.job_get_from_pid1(pid, Pin::new(&mut job_pos))
.as_ref()
};
if job.is_some() {
send_to_bg(parser, streams, cmd, job_pos);
} else {
streams
.err
.append(wgettext_fmt!("%ls: Could not find job '%d'\n", cmd, pid));
}
}
return STATUS_CMD_OK;
}

View File

@ -1,158 +0,0 @@
// Implementation of the block builtin.
use super::shared::{
builtin_missing_argument, builtin_print_help, io_streams_t, STATUS_CMD_ERROR, STATUS_CMD_OK,
STATUS_INVALID_ARGS,
};
use crate::{
builtins::shared::builtin_unknown_option,
ffi::{parser_t, Repin},
wchar::{wstr, L},
wgetopt::{wgetopter_t, wopt, woption, woption_argument_t},
wutil::wgettext_fmt,
};
use libc::c_int;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Scope {
Unset,
Global,
Local,
}
impl Default for Scope {
fn default() -> Self {
Self::Unset
}
}
#[derive(Debug, Clone, Copy, Default)]
struct Options {
scope: Scope,
erase: bool,
print_help: bool,
}
fn parse_options(
args: &mut [&wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Result<(Options, usize), Option<c_int>> {
let cmd = args[0];
const SHORT_OPTS: &wstr = L!(":eghl");
const LONG_OPTS: &[woption] = &[
wopt(L!("erase"), woption_argument_t::no_argument, 'e'),
wopt(L!("local"), woption_argument_t::no_argument, 'l'),
wopt(L!("global"), woption_argument_t::no_argument, 'g'),
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
];
let mut opts = Options::default();
let mut w = wgetopter_t::new(SHORT_OPTS, LONG_OPTS, args);
while let Some(c) = w.wgetopt_long() {
match c {
'h' => {
opts.print_help = true;
}
'g' => {
opts.scope = Scope::Global;
}
'l' => {
opts.scope = Scope::Local;
}
'e' => {
opts.erase = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.woptind - 1], false);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
builtin_unknown_option(parser, streams, cmd, args[w.woptind - 1], false);
return Err(STATUS_INVALID_ARGS);
}
_ => {
panic!("unexpected retval from wgetopt_long");
}
}
}
Ok((opts, w.woptind))
}
/// The block builtin, used for temporarily blocking events.
pub fn block(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let cmd = args[0];
let opts = match parse_options(args, parser, streams) {
Ok((opts, _)) => opts,
Err(err @ Some(_)) if err != STATUS_CMD_OK => return err,
Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"),
};
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
if opts.erase {
if opts.scope != Scope::Unset {
streams.err.append(wgettext_fmt!(
"%ls: Can not specify scope when removing block\n",
cmd
));
return STATUS_INVALID_ARGS;
}
if parser.ffi_global_event_blocks() == 0 {
streams
.err
.append(wgettext_fmt!("%ls: No blocks defined\n", cmd));
return STATUS_CMD_ERROR;
}
parser.pin().ffi_decr_global_event_blocks();
return STATUS_CMD_OK;
}
let mut block_idx = 0;
let mut block = unsafe { parser.pin().block_at_index1(block_idx).as_mut() };
match opts.scope {
Scope::Local => {
// If this is the outermost block, then we're global
if block_idx + 1 >= parser.ffi_blocks_size() {
block = None;
}
}
Scope::Global => {
block = None;
}
Scope::Unset => {
loop {
block = if let Some(block) = block.as_mut() {
if !block.is_function_call() {
break;
}
// Set it in function scope
block_idx += 1;
unsafe { parser.pin().block_at_index1(block_idx).as_mut() }
} else {
break;
}
}
}
}
if let Some(block) = block.as_mut() {
block.pin().ffi_incr_event_blocks();
} else {
parser.pin().ffi_incr_global_event_blocks();
}
return STATUS_CMD_OK;
}

View File

@ -1,93 +0,0 @@
// Implementation of the contains builtin.
use super::shared::{
builtin_missing_argument, builtin_print_help, io_streams_t, STATUS_CMD_ERROR, STATUS_CMD_OK,
STATUS_INVALID_ARGS,
};
use crate::builtins::shared::builtin_unknown_option;
use crate::ffi::parser_t;
use crate::wchar::{wstr, L};
use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t};
use crate::wutil::wgettext_fmt;
use libc::c_int;
#[derive(Debug, Clone, Copy, Default)]
struct Options {
print_help: bool,
print_index: bool,
}
fn parse_options(
args: &mut [&wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Result<(Options, usize), Option<c_int>> {
let cmd = args[0];
const SHORT_OPTS: &wstr = L!("+:hi");
const LONG_OPTS: &[woption] = &[
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
wopt(L!("index"), woption_argument_t::no_argument, 'i'),
];
let mut opts = Options::default();
let mut w = wgetopter_t::new(SHORT_OPTS, LONG_OPTS, args);
while let Some(c) = w.wgetopt_long() {
match c {
'h' => opts.print_help = true,
'i' => opts.print_index = true,
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.woptind - 1], false);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
builtin_unknown_option(parser, streams, cmd, args[w.woptind - 1], false);
return Err(STATUS_INVALID_ARGS);
}
_ => {
panic!("unexpected retval from wgetopt_long");
}
}
}
Ok((opts, w.woptind))
}
/// Implementation of the builtin contains command, used to check if a specified string is part of
/// a list.
pub fn contains(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let cmd = args[0];
let (opts, optind) = match parse_options(args, parser, streams) {
Ok((opts, optind)) => (opts, optind),
Err(err @ Some(_)) if err != STATUS_CMD_OK => return err,
Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"),
};
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
let needle = args.get(optind);
if let Some(needle) = needle {
for (i, arg) in args[optind..].iter().enumerate().skip(1) {
if needle == arg {
if opts.print_index {
streams.out.append(wgettext_fmt!("%d\n", i));
}
return STATUS_CMD_OK;
}
}
} else {
streams
.err
.append(wgettext_fmt!("%ls: Key not specified\n", cmd));
}
return STATUS_CMD_ERROR;
}

View File

@ -1,232 +0,0 @@
//! Implementation of the echo builtin.
use libc::c_int;
use super::shared::{builtin_missing_argument, io_streams_t, STATUS_CMD_OK, STATUS_INVALID_ARGS};
use crate::ffi::parser_t;
use crate::wchar::{wchar_literal_byte, wstr, WString, L};
use crate::wgetopt::{wgetopter_t, woption};
#[derive(Debug, Clone, Copy)]
struct Options {
print_newline: bool,
print_spaces: bool,
interpret_special_chars: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
print_newline: true,
print_spaces: true,
interpret_special_chars: false,
}
}
}
fn parse_options(
args: &mut [&wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Result<(Options, usize), Option<c_int>> {
let cmd = args[0];
const SHORT_OPTS: &wstr = L!("+:Eens");
const LONG_OPTS: &[woption] = &[];
let mut opts = Options::default();
let mut oldopts = opts;
let mut oldoptind = 0;
let mut w = wgetopter_t::new(SHORT_OPTS, LONG_OPTS, args);
while let Some(c) = w.wgetopt_long() {
match c {
'n' => opts.print_newline = false,
'e' => opts.interpret_special_chars = true,
's' => opts.print_spaces = false,
'E' => opts.interpret_special_chars = false,
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.woptind - 1], true);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
return Ok((oldopts, w.woptind - 1));
}
_ => {
panic!("unexpected retval from wgetopter::wgetopt_long()");
}
}
// Super cheesy: We keep an old copy of the option state around,
// so we can revert it in case we get an argument like
// "-n foo".
// We need to keep it one out-of-date so we can ignore the *last* option.
// (this might be an issue in wgetopt, but that's a whole other can of worms
// and really only occurs with our weird "put it back" option parsing)
if w.woptind == oldoptind + 2 {
oldopts = opts;
oldoptind = w.woptind;
}
}
Ok((opts, w.woptind))
}
/// Parse a numeric escape sequence in `s`, returning the number of characters consumed and the
/// resulting value. Supported escape sequences:
///
/// - `0nnn`: octal value, zero to three digits
/// - `nnn`: octal value, one to three digits
/// - `xhh`: hex value, one to two digits
fn parse_numeric_sequence<I>(chars: I) -> Option<(usize, u8)>
where
I: IntoIterator<Item = char>,
{
let mut chars = chars.into_iter().peekable();
// the first character of the numeric part of the sequence
let mut start = 0;
let mut base: u8 = 0;
let mut max_digits = 0;
let first = *chars.peek()?;
if first.is_digit(8) {
// Octal escape
base = 8;
// If the first digit is a 0, we allow four digits (including that zero); otherwise, we
// allow 3.
max_digits = if first == '0' { 4 } else { 3 };
} else if first == 'x' {
// Hex escape
base = 16;
max_digits = 2;
// Skip the x
start = 1;
};
if base == 0 {
return None;
}
let mut val = 0;
let mut consumed = start;
for digit in chars
.skip(start)
.take(max_digits)
.map_while(|c| c.to_digit(base.into()))
{
// base is either 8 or 16, so digit can never be >255
let digit = u8::try_from(digit).unwrap();
val = val * base + digit;
consumed += 1;
}
// We succeeded if we consumed at least one digit.
if consumed > 0 {
Some((consumed, val))
} else {
None
}
}
/// The echo builtin.
///
/// Bash only respects `-n` if it's the first argument. We'll do the same. We also support a new,
/// fish specific, option `-s` to mean "no spaces".
pub fn echo(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let (opts, optind) = match parse_options(args, parser, streams) {
Ok((opts, optind)) => (opts, optind),
Err(err @ Some(_)) if err != STATUS_CMD_OK => return err,
Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"),
};
// The special character \c can be used to indicate no more output.
let mut output_stopped = false;
// We buffer output so we can write in one go,
// this matters when writing to an fd.
let mut out = WString::new();
let args_to_echo = &args[optind..];
'outer: for (idx, arg) in args_to_echo.iter().enumerate() {
if opts.print_spaces && idx > 0 {
out.push(' ');
}
let mut chars = arg.chars().peekable();
while let Some(c) = chars.next() {
if !opts.interpret_special_chars || c != '\\' {
// Not an escape.
out.push(c);
continue;
}
let Some(next_char) = chars.peek() else {
// Incomplete escape sequence is echoed verbatim
out.push('\\');
break;
};
// Most escapes consume one character in addition to the backslash; the numeric
// sequences may consume more, while an unrecognized escape sequence consumes none.
let mut consumed = 1;
let escaped = match next_char {
'a' => '\x07',
'b' => '\x08',
'e' => '\x1B',
'f' => '\x0C',
'n' => '\n',
'r' => '\r',
't' => '\t',
'v' => '\x0B',
'\\' => '\\',
'c' => {
output_stopped = true;
break 'outer;
}
_ => {
// Octal and hex escape sequences.
if let Some((digits_consumed, narrow_val)) =
parse_numeric_sequence(chars.clone())
{
consumed = digits_consumed;
// The narrow_val is a literal byte that we want to output (#1894).
wchar_literal_byte(narrow_val)
} else {
consumed = 0;
'\\'
}
}
};
// Skip over characters that were part of this escape sequence (after the backslash
// that was consumed by the `while` loop).
// TODO: `Iterator::advance_by()`: https://github.com/rust-lang/rust/issues/77404
for _ in 0..consumed {
let _ = chars.next();
}
out.push(escaped);
}
}
if opts.print_newline && !output_stopped {
out.push('\n');
}
if !out.is_empty() {
streams.out.append(out);
}
STATUS_CMD_OK
}

View File

@ -1,46 +0,0 @@
use libc::c_int;
use widestring_suffix::widestrs;
use super::shared::{
builtin_print_help, io_streams_t, HelpOnlyCmdOpts, STATUS_CMD_OK, STATUS_INVALID_ARGS,
};
use crate::event;
use crate::ffi::parser_t;
use crate::wchar::{wstr, WString};
use crate::wutil::format::printf::sprintf;
#[widestrs]
pub fn emit(
parser: &mut parser_t,
streams: &mut io_streams_t,
argv: &mut [&wstr],
) -> Option<c_int> {
let cmd = argv[0];
let opts = match HelpOnlyCmdOpts::parse(argv, parser, streams) {
Ok(opts) => opts,
Err(err @ Some(_)) if err != STATUS_CMD_OK => return err,
Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"),
};
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
let Some(event_name) = argv.get(opts.optind) else {
streams.err.append(&sprintf!("%ls: expected event name\n"L, cmd));
return STATUS_INVALID_ARGS;
};
event::fire_generic(
parser,
(*event_name).to_owned(),
argv[opts.optind + 1..]
.iter()
.map(|&s| WString::from(s))
.collect(),
);
STATUS_CMD_OK
}

View File

@ -1,26 +0,0 @@
use libc::c_int;
use super::r#return::parse_return_value;
use super::shared::io_streams_t;
use crate::ffi::parser_t;
use crate::wchar::wstr;
/// Function for handling the exit builtin.
pub fn exit(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let retval = match parse_return_value(args, parser, streams) {
Ok(v) => v,
Err(e) => return e,
};
// Mark that we are exiting in the parser.
// TODO: in concurrent mode this won't successfully exit a pipeline, as there are other parsers
// involved. That is, `exit | sleep 1000` may not exit as hoped. Need to rationalize what
// behavior we want here.
parser.libdata_pod().exit_current_script = true;
return Some(retval);
}

View File

@ -1,14 +0,0 @@
pub mod shared;
pub mod abbr;
pub mod bg;
pub mod block;
pub mod contains;
pub mod echo;
pub mod emit;
pub mod exit;
pub mod pwd;
pub mod random;
pub mod realpath;
pub mod r#return;
pub mod wait;

View File

@ -1,80 +0,0 @@
//! Implementation of the pwd builtin.
use errno::errno;
use libc::c_int;
use crate::{
builtins::shared::{io_streams_t, BUILTIN_ERR_ARG_COUNT1},
env::flags::EnvMode,
ffi::parser_t,
wchar::{wstr, WString, L},
wchar_ffi::{WCharFromFFI, WCharToFFI},
wgetopt::{wgetopter_t, wopt, woption, woption_argument_t::no_argument},
wutil::{wgettext_fmt, wrealpath},
};
use super::shared::{
builtin_print_help, builtin_unknown_option, STATUS_CMD_ERROR, STATUS_CMD_OK,
STATUS_INVALID_ARGS,
};
// The pwd builtin. Respect -P to resolve symbolic links. Respect -L to not do that (the default).
const short_options: &wstr = L!("LPh");
const long_options: &[woption] = &[
wopt(L!("help"), no_argument, 'h'),
wopt(L!("logical"), no_argument, 'L'),
wopt(L!("physical"), no_argument, 'P'),
];
pub fn pwd(parser: &mut parser_t, streams: &mut io_streams_t, argv: &mut [&wstr]) -> Option<c_int> {
let cmd = argv[0];
let argc = argv.len();
let mut resolve_symlinks = false;
let mut w = wgetopter_t::new(short_options, long_options, argv);
while let Some(opt) = w.wgetopt_long() {
match opt {
'L' => resolve_symlinks = false,
'P' => resolve_symlinks = true,
'h' => {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
'?' => {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], false);
return STATUS_INVALID_ARGS;
}
_ => panic!("unexpected retval from wgetopt_long"),
}
}
if w.woptind != argc {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_ARG_COUNT1, cmd, 0, argc - 1));
return STATUS_INVALID_ARGS;
}
let mut pwd = WString::new();
let tmp = parser
.vars1()
.get_or_null(&L!("PWD").to_ffi(), EnvMode::DEFAULT.bits());
if !tmp.is_null() {
pwd = tmp.as_string().from_ffi();
}
if resolve_symlinks {
if let Some(real_pwd) = wrealpath(&pwd) {
pwd = real_pwd;
} else {
streams.err.append(wgettext_fmt!(
"%ls: realpath failed: %s\n",
cmd,
errno().to_string()
));
return STATUS_CMD_ERROR;
}
}
if pwd.is_empty() {
return STATUS_CMD_ERROR;
}
streams.out.append(pwd + L!("\n"));
return STATUS_CMD_OK;
}

View File

@ -1,172 +0,0 @@
use libc::c_int;
use crate::builtins::shared::{
builtin_missing_argument, builtin_print_help, builtin_unknown_option, io_streams_t,
STATUS_CMD_OK, STATUS_INVALID_ARGS,
};
use crate::ffi::parser_t;
use crate::wchar::{wstr, L};
use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t};
use crate::wutil::{self, fish_wcstoi_radix_all, format::printf::sprintf, wgettext_fmt};
use num_traits::PrimInt;
use once_cell::sync::Lazy;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use std::sync::Mutex;
static RNG: Lazy<Mutex<SmallRng>> = Lazy::new(|| Mutex::new(SmallRng::from_entropy()));
pub fn random(
parser: &mut parser_t,
streams: &mut io_streams_t,
argv: &mut [&wstr],
) -> Option<c_int> {
let cmd = argv[0];
let argc = argv.len();
let print_hints = false;
const shortopts: &wstr = L!("+:h");
const longopts: &[woption] = &[wopt(L!("help"), woption_argument_t::no_argument, 'h')];
let mut w = wgetopter_t::new(shortopts, longopts, argv);
while let Some(c) = w.wgetopt_long() {
match c {
'h' => {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], print_hints);
return STATUS_INVALID_ARGS;
}
'?' => {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], print_hints);
return STATUS_INVALID_ARGS;
}
_ => {
panic!("unexpected retval from wgeopter.next()");
}
}
}
let mut start = 0;
let mut end = 32767;
let mut step = 1;
let arg_count = argc - w.woptind;
let i = w.woptind;
if arg_count >= 1 && argv[i] == "choice" {
if arg_count == 1 {
streams
.err
.append(wgettext_fmt!("%ls: nothing to choose from\n", cmd,));
return STATUS_INVALID_ARGS;
}
let rand = RNG.lock().unwrap().gen_range(0..arg_count - 1);
streams
.out
.append(sprintf!(L!("%ls\n"), argv[i + 1 + rand]));
return STATUS_CMD_OK;
}
fn parse<T: PrimInt>(
streams: &mut io_streams_t,
cmd: &wstr,
num: &wstr,
) -> Result<T, wutil::Error> {
let res = fish_wcstoi_radix_all(num.chars(), None, true);
if res.is_err() {
streams
.err
.append(wgettext_fmt!("%ls: %ls: invalid integer\n", cmd, num,));
}
return res;
}
match arg_count {
0 => {
// Keep the defaults
}
1 => {
// Seed the engine persistently
let num = parse::<i64>(streams, cmd, argv[i]);
match num {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => {
let mut engine = RNG.lock().unwrap();
*engine = SmallRng::seed_from_u64(x as u64);
}
}
return STATUS_CMD_OK;
}
2 => {
// start is first, end is second
match parse::<i64>(streams, cmd, argv[i]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => start = x,
}
match parse::<i64>(streams, cmd, argv[i + 1]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => end = x,
}
}
3 => {
// start, step, end
match parse::<i64>(streams, cmd, argv[i]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => start = x,
}
// start, step, end
match parse::<u64>(streams, cmd, argv[i + 1]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(0) => {
streams
.err
.append(wgettext_fmt!("%ls: STEP must be a positive integer\n", cmd,));
return STATUS_INVALID_ARGS;
}
Ok(x) => step = x,
}
match parse::<i64>(streams, cmd, argv[i + 2]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => end = x,
}
}
_ => {
streams
.err
.append(wgettext_fmt!("%ls: too many arguments\n", cmd,));
return Some(1);
}
}
if end <= start {
streams
.err
.append(wgettext_fmt!("%ls: END must be greater than START\n", cmd,));
return STATUS_INVALID_ARGS;
}
// Using abs_diff() avoids an i64 overflow if start is i64::MIN and end is i64::MAX
let possibilities = end.abs_diff(start) / step;
if possibilities == 0 {
streams.err.append(wgettext_fmt!(
"%ls: range contains only one possible value\n",
cmd,
));
return STATUS_INVALID_ARGS;
}
let rand = {
let mut engine = RNG.lock().unwrap();
engine.gen_range(0..=possibilities)
};
// Safe because end was a valid i64 and the result here is in the range start..=end.
let result: i64 = start.checked_add_unsigned(rand * step).unwrap();
streams.out.append(sprintf!(L!("%d\n"), result));
return STATUS_CMD_OK;
}

View File

@ -1,143 +0,0 @@
//! Implementation of the realpath builtin.
use errno::errno;
use libc::c_int;
use crate::{
ffi::parser_t,
path::path_apply_working_directory,
wchar::{wstr, WExt, L},
wchar_ffi::WCharFromFFI,
wgetopt::{wgetopter_t, wopt, woption, woption_argument_t::no_argument},
wutil::{normalize_path, wgettext_fmt, wrealpath},
};
use super::shared::{
builtin_missing_argument, builtin_print_help, builtin_unknown_option, io_streams_t,
BUILTIN_ERR_ARG_COUNT1, STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS,
};
#[derive(Default)]
struct Options {
print_help: bool,
no_symlinks: bool,
}
const short_options: &wstr = L!("+:hs");
const long_options: &[woption] = &[
wopt(L!("no-symlinks"), no_argument, 's'),
wopt(L!("help"), no_argument, 'h'),
];
fn parse_options(
args: &mut [&wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Result<(Options, usize), Option<c_int>> {
let cmd = args[0];
let mut opts = Options::default();
let mut w = wgetopter_t::new(short_options, long_options, args);
while let Some(c) = w.wgetopt_long() {
match c {
's' => opts.no_symlinks = true,
'h' => opts.print_help = true,
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.woptind - 1], false);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
builtin_unknown_option(parser, streams, cmd, args[w.woptind - 1], false);
return Err(STATUS_INVALID_ARGS);
}
_ => panic!("unexpected retval from wgetopt_long"),
}
}
Ok((opts, w.woptind))
}
/// An implementation of the external realpath command. Doesn't support any options.
/// In general scripts shouldn't invoke this directly. They should just use `realpath` which
/// will fallback to this builtin if an external command cannot be found.
pub fn realpath(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let cmd = args[0];
let (opts, optind) = match parse_options(args, parser, streams) {
Ok((opts, optind)) => (opts, optind),
Err(err @ Some(_)) if err != STATUS_CMD_OK => return err,
Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"),
};
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
// TODO: allow arbitrary args. `realpath *` should print many paths
if optind + 1 != args.len() {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT1,
cmd,
0,
args.len() - 1
));
builtin_print_help(parser, streams, cmd);
return STATUS_INVALID_ARGS;
}
let arg = args[optind];
if !opts.no_symlinks {
if let Some(real_path) = wrealpath(arg) {
streams.out.append(real_path);
} else {
let errno = errno();
if errno.0 != 0 {
// realpath() just couldn't do it. Report the error and make it clear
// this is an error from our builtin, not the system's realpath.
streams.err.append(wgettext_fmt!(
"builtin %ls: %ls: %s\n",
cmd,
arg,
errno.to_string()
));
} else {
// Who knows. Probably a bug in our wrealpath() implementation.
streams
.err
.append(wgettext_fmt!("builtin %ls: Invalid arg: %ls\n", cmd, arg));
}
return STATUS_CMD_ERROR;
}
} else {
// We need to get the *physical* pwd here.
let realpwd = wrealpath(&parser.vars1().get_pwd_slash().from_ffi());
if let Some(realpwd) = realpwd {
let absolute_arg = if arg.starts_with(L!("/")) {
arg.to_owned()
} else {
path_apply_working_directory(arg, &realpwd)
};
streams.out.append(normalize_path(&absolute_arg, false));
} else {
streams.err.append(wgettext_fmt!(
"builtin %ls: realpath failed: %s\n",
cmd,
errno().to_string()
));
return STATUS_CMD_ERROR;
}
}
streams.out.append(L!("\n"));
STATUS_CMD_OK
}

View File

@ -1,131 +0,0 @@
// Implementation of the return builtin.
use libc::c_int;
use num_traits::abs;
use super::shared::{
builtin_missing_argument, builtin_print_error_trailer, builtin_print_help, io_streams_t,
BUILTIN_ERR_NOT_NUMBER, STATUS_CMD_OK, STATUS_INVALID_ARGS,
};
use crate::builtins::shared::BUILTIN_ERR_TOO_MANY_ARGUMENTS;
use crate::ffi::parser_t;
use crate::wchar::{wstr, L};
use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t};
use crate::wutil::fish_wcstoi;
use crate::wutil::wgettext_fmt;
#[derive(Debug, Clone, Copy, Default)]
struct Options {
print_help: bool,
}
fn parse_options(
args: &mut [&wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Result<(Options, usize), Option<c_int>> {
let cmd = args[0];
const SHORT_OPTS: &wstr = L!(":h");
const LONG_OPTS: &[woption] = &[wopt(L!("help"), woption_argument_t::no_argument, 'h')];
let mut opts = Options::default();
let mut w = wgetopter_t::new(SHORT_OPTS, LONG_OPTS, args);
while let Some(c) = w.wgetopt_long() {
match c {
'h' => opts.print_help = true,
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.woptind - 1], true);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
// We would normally invoke builtin_unknown_option() and return an error.
// But for this command we want to let it try and parse the value as a negative
// return value.
return Ok((opts, w.woptind - 1));
}
_ => {
panic!("unexpected retval from wgetopt_long");
}
}
}
Ok((opts, w.woptind))
}
/// Function for handling the return builtin.
pub fn r#return(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let mut retval = match parse_return_value(args, parser, streams) {
Ok(v) => v,
Err(e) => return e,
};
let has_function_block = parser.ffi_has_funtion_block();
// *nix does not support negative return values, but our `return` builtin happily accepts being
// called with negative literals (e.g. `return -1`).
// Map negative values to (256 - their absolute value). This prevents `return -1` from
// evaluating to a `$status` of 0 and keeps us from running into undefined behavior by trying to
// left shift a negative value in W_EXITCODE().
if retval < 0 {
retval = 256 - (abs(retval) % 256);
}
// If we're not in a function, exit the current script (but not an interactive shell).
if !has_function_block {
let ld = parser.libdata_pod();
if !ld.is_interactive {
ld.exit_current_script = true;
}
return Some(retval);
}
// Mark a return in the libdata.
parser.libdata_pod().returning = true;
return Some(retval);
}
pub fn parse_return_value(
args: &mut [&wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Result<i32, Option<c_int>> {
let cmd = args[0];
let (opts, optind) = match parse_options(args, parser, streams) {
Ok((opts, optind)) => (opts, optind),
Err(err @ Some(_)) if err != STATUS_CMD_OK => return Err(err),
Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"),
};
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return Err(STATUS_CMD_OK);
}
if optind + 1 < args.len() {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd));
builtin_print_error_trailer(parser, streams, cmd);
return Err(STATUS_INVALID_ARGS);
}
if optind == args.len() {
Ok(parser.get_last_status().into())
} else {
match fish_wcstoi(args[optind].chars()) {
Ok(i) => Ok(i),
Err(_e) => {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, args[1]));
builtin_print_error_trailer(parser, streams, cmd);
return Err(STATUS_INVALID_ARGS);
}
}
}
}

View File

@ -1,234 +0,0 @@
use crate::builtins::wait;
use crate::ffi::{self, parser_t, wcharz_t, Repin, RustBuiltin};
use crate::wchar::{self, wstr, L};
use crate::wchar_ffi::{c_str, empty_wstring};
use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t};
use libc::c_int;
use std::pin::Pin;
#[cxx::bridge]
mod builtins_ffi {
extern "C++" {
include!("wutil.h");
include!("parser.h");
include!("builtin.h");
type wcharz_t = crate::ffi::wcharz_t;
type parser_t = crate::ffi::parser_t;
type io_streams_t = crate::ffi::io_streams_t;
type RustBuiltin = crate::ffi::RustBuiltin;
}
extern "Rust" {
fn rust_run_builtin(
parser: Pin<&mut parser_t>,
streams: Pin<&mut io_streams_t>,
cpp_args: &Vec<wcharz_t>,
builtin: RustBuiltin,
status_code: &mut i32,
) -> bool;
}
impl Vec<wcharz_t> {}
}
/// Error message when too many arguments are supplied to a builtin.
pub const BUILTIN_ERR_TOO_MANY_ARGUMENTS: &str = "%ls: too many arguments\n";
/// Error message when integer expected
pub const BUILTIN_ERR_NOT_NUMBER: &str = "%ls: %ls: invalid integer\n";
pub const BUILTIN_ERR_ARG_COUNT1: &str = "%ls: expected %d arguments; got %d\n";
/// A handy return value for successful builtins.
pub const STATUS_CMD_OK: Option<c_int> = Some(0);
/// The status code used for failure exit in a command (but not if the args were invalid).
pub const STATUS_CMD_ERROR: Option<c_int> = Some(1);
/// A handy return value for invalid args.
pub const STATUS_INVALID_ARGS: Option<c_int> = Some(2);
/// A wrapper around output_stream_t.
pub struct output_stream_t(*mut ffi::output_stream_t);
impl output_stream_t {
/// \return the underlying output_stream_t.
fn ffi(&mut self) -> Pin<&mut ffi::output_stream_t> {
unsafe { (*self.0).pin() }
}
/// Append a &wtr or WString.
pub fn append<Str: AsRef<wstr>>(&mut self, s: Str) -> bool {
self.ffi().append1(c_str!(s))
}
}
// Convenience wrappers around C++ io_streams_t.
pub struct io_streams_t {
streams: *mut builtins_ffi::io_streams_t,
pub out: output_stream_t,
pub err: output_stream_t,
}
impl io_streams_t {
pub fn new(mut streams: Pin<&mut builtins_ffi::io_streams_t>) -> io_streams_t {
let out = output_stream_t(streams.as_mut().get_out().unpin());
let err = output_stream_t(streams.as_mut().get_err().unpin());
let streams = streams.unpin();
io_streams_t { streams, out, err }
}
pub fn ffi_pin(&mut self) -> Pin<&mut builtins_ffi::io_streams_t> {
unsafe { Pin::new_unchecked(&mut *self.streams) }
}
pub fn ffi_ref(&self) -> &builtins_ffi::io_streams_t {
unsafe { &*self.streams }
}
}
fn rust_run_builtin(
parser: Pin<&mut parser_t>,
streams: Pin<&mut builtins_ffi::io_streams_t>,
cpp_args: &Vec<wcharz_t>,
builtin: RustBuiltin,
status_code: &mut i32,
) -> bool {
let mut storage = Vec::<wchar::WString>::new();
for arg in cpp_args {
storage.push(arg.into());
}
let mut args = Vec::new();
for arg in &storage {
args.push(arg.as_utfstr());
}
let streams = &mut io_streams_t::new(streams);
match run_builtin(parser.unpin(), streams, args.as_mut_slice(), builtin) {
None => false,
Some(status) => {
*status_code = status;
true
}
}
}
pub fn run_builtin(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
builtin: RustBuiltin,
) -> Option<c_int> {
match builtin {
RustBuiltin::Abbr => super::abbr::abbr(parser, streams, args),
RustBuiltin::Bg => super::bg::bg(parser, streams, args),
RustBuiltin::Block => super::block::block(parser, streams, args),
RustBuiltin::Contains => super::contains::contains(parser, streams, args),
RustBuiltin::Echo => super::echo::echo(parser, streams, args),
RustBuiltin::Emit => super::emit::emit(parser, streams, args),
RustBuiltin::Exit => super::exit::exit(parser, streams, args),
RustBuiltin::Pwd => super::pwd::pwd(parser, streams, args),
RustBuiltin::Random => super::random::random(parser, streams, args),
RustBuiltin::Realpath => super::realpath::realpath(parser, streams, args),
RustBuiltin::Return => super::r#return::r#return(parser, streams, args),
RustBuiltin::Wait => wait::wait(parser, streams, args),
}
}
// Covers of these functions that take care of the pinning, etc.
// These all return STATUS_INVALID_ARGS.
pub fn builtin_missing_argument(
parser: &mut parser_t,
streams: &mut io_streams_t,
cmd: &wstr,
opt: &wstr,
print_hints: bool,
) {
ffi::builtin_missing_argument(
parser.pin(),
streams.ffi_pin(),
c_str!(cmd),
c_str!(opt),
print_hints,
);
}
pub fn builtin_unknown_option(
parser: &mut parser_t,
streams: &mut io_streams_t,
cmd: &wstr,
opt: &wstr,
print_hints: bool,
) {
ffi::builtin_unknown_option(
parser.pin(),
streams.ffi_pin(),
c_str!(cmd),
c_str!(opt),
print_hints,
);
}
pub fn builtin_print_help(parser: &mut parser_t, streams: &io_streams_t, cmd: &wstr) {
ffi::builtin_print_help(
parser.pin(),
streams.ffi_ref(),
c_str!(cmd),
empty_wstring(),
);
}
pub fn builtin_print_error_trailer(parser: &mut parser_t, streams: &mut io_streams_t, cmd: &wstr) {
ffi::builtin_print_error_trailer(parser.pin(), streams.err.ffi(), c_str!(cmd));
}
pub struct HelpOnlyCmdOpts {
pub print_help: bool,
pub optind: usize,
}
impl HelpOnlyCmdOpts {
pub fn parse(
args: &mut [&wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Result<Self, Option<c_int>> {
let cmd = args[0];
let print_hints = true;
const shortopts: &wstr = L!("+:h");
const longopts: &[woption] = &[wopt(L!("help"), woption_argument_t::no_argument, 'h')];
let mut print_help = false;
let mut w = wgetopter_t::new(shortopts, longopts, args);
while let Some(c) = w.wgetopt_long() {
match c {
'h' => {
print_help = true;
}
':' => {
builtin_missing_argument(
parser,
streams,
cmd,
args[w.woptind - 1],
print_hints,
);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
builtin_unknown_option(parser, streams, cmd, args[w.woptind - 1], print_hints);
return Err(STATUS_INVALID_ARGS);
}
_ => {
panic!("unexpected retval from wgetopter::wgetopt_long()");
}
}
}
Ok(HelpOnlyCmdOpts {
print_help,
optind: w.woptind,
})
}
}

View File

@ -1,246 +0,0 @@
use libc::{c_int, pid_t};
use crate::builtins::shared::{
builtin_missing_argument, builtin_print_help, builtin_unknown_option, io_streams_t,
STATUS_CMD_OK, STATUS_INVALID_ARGS,
};
use crate::ffi::{job_t, parser_t, proc_wait_any, wait_handle_ref_t, Repin};
use crate::signal::sigchecker_t;
use crate::wchar::{widestrs, wstr};
use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t};
use crate::wutil::{self, fish_wcstoi, wgettext_fmt};
/// \return true if we can wait on a job.
fn can_wait_on_job(j: &cxx::SharedPtr<job_t>) -> bool {
j.is_constructed() && !j.is_foreground() && !j.is_stopped()
}
/// \return true if a wait handle matches a pid or a process name.
/// For convenience, this returns false if the wait handle is null.
fn wait_handle_matches(query: WaitHandleQuery, wh: &wait_handle_ref_t) -> bool {
if wh.is_null() {
return false;
}
match query {
WaitHandleQuery::Pid(pid) => wh.get_pid().0 == pid,
WaitHandleQuery::ProcName(proc_name) => proc_name == wh.get_base_name(),
}
}
/// \return true if all chars are numeric.
fn iswnumeric(s: &wstr) -> bool {
s.chars().all(|c| c.is_ascii_digit())
}
// Hack to copy wait handles into a vector.
fn get_wait_handle_list(parser: &parser_t) -> Vec<wait_handle_ref_t> {
let mut handles = Vec::new();
let whs = parser.get_wait_handles1();
for idx in 0..whs.size() {
handles.push(whs.get(idx));
}
handles
}
#[derive(Copy, Clone)]
enum WaitHandleQuery<'a> {
Pid(pid_t),
ProcName(&'a wstr),
}
/// Walk the list of jobs, looking for a process with the given pid or proc name.
/// Append all matching wait handles to \p handles.
/// \return true if we found a matching job (even if not waitable), false if not.
fn find_wait_handles(
query: WaitHandleQuery<'_>,
parser: &parser_t,
handles: &mut Vec<wait_handle_ref_t>,
) -> bool {
// Has a job already completed?
// TODO: we can avoid traversing this list if searching by pid.
let mut matched = false;
for wh in get_wait_handle_list(parser) {
if wait_handle_matches(query, &wh) {
handles.push(wh);
matched = true;
}
}
// Is there a running job match?
for j in parser.get_jobs() {
// We want to set 'matched' to true if we could have matched, even if the job was stopped.
let provide_handle = can_wait_on_job(j);
for proc in j.get_procs() {
let wh = proc.pin_mut().make_wait_handle(j.get_internal_job_id());
if wait_handle_matches(query, &wh) {
matched = true;
if provide_handle {
handles.push(wh);
}
}
}
}
matched
}
fn get_all_wait_handles(parser: &parser_t) -> Vec<wait_handle_ref_t> {
let mut result = Vec::new();
// Get wait handles for reaped jobs.
let wait_handles = parser.get_wait_handles1();
for idx in 0..wait_handles.size() {
result.push(wait_handles.get(idx));
}
// Get wait handles for running jobs.
for j in parser.get_jobs() {
if !can_wait_on_job(j) {
continue;
}
for proc_ptr in j.get_procs().iter_mut() {
let proc = proc_ptr.pin_mut();
let wh = proc.make_wait_handle(j.get_internal_job_id());
if !wh.is_null() {
result.push(wh);
}
}
}
result
}
fn is_completed(wh: &wait_handle_ref_t) -> bool {
wh.is_completed()
}
/// Wait for the given wait handles to be marked as completed.
/// If \p any_flag is set, wait for the first one; otherwise wait for all.
/// \return a status code.
fn wait_for_completion(
parser: &mut parser_t,
whs: &[wait_handle_ref_t],
any_flag: bool,
) -> Option<c_int> {
if whs.is_empty() {
return Some(0);
}
let mut sigint = sigchecker_t::new_sighupint();
loop {
let finished = if any_flag {
whs.iter().any(is_completed)
} else {
whs.iter().all(is_completed)
};
if finished {
// Remove completed wait handles (at most 1 if any_flag is set).
for wh in whs {
if is_completed(wh) {
parser.pin().get_wait_handles().remove(wh);
if any_flag {
break;
}
}
}
return Some(0);
}
if sigint.check() {
return Some(128 + libc::SIGINT);
}
proc_wait_any(parser.pin());
}
}
#[widestrs]
pub fn wait(
parser: &mut parser_t,
streams: &mut io_streams_t,
argv: &mut [&wstr],
) -> Option<c_int> {
let cmd = argv[0];
let argc = argv.len();
let mut any_flag = false; // flag for -n option
let mut print_help = false;
let print_hints = false;
const shortopts: &wstr = ":nh"L;
const longopts: &[woption] = &[
wopt("any"L, woption_argument_t::no_argument, 'n'),
wopt("help"L, woption_argument_t::no_argument, 'h'),
];
let mut w = wgetopter_t::new(shortopts, longopts, argv);
while let Some(c) = w.wgetopt_long() {
match c {
'n' => {
any_flag = true;
}
'h' => {
print_help = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], print_hints);
return STATUS_INVALID_ARGS;
}
'?' => {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], print_hints);
return STATUS_INVALID_ARGS;
}
_ => {
panic!("unexpected retval from wgeopter.next()");
}
}
}
if print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
if w.woptind == argc {
// No jobs specified.
// Note this may succeed with an empty wait list.
return wait_for_completion(parser, &get_all_wait_handles(parser), any_flag);
}
// Get the list of wait handles for our waiting.
let mut wait_handles: Vec<wait_handle_ref_t> = Vec::new();
for i in w.woptind..argc {
if iswnumeric(argv[i]) {
// argument is pid
let mpid: Result<pid_t, wutil::Error> = fish_wcstoi(argv[i].chars());
if mpid.is_err() || mpid.unwrap() <= 0 {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is not a valid process id\n",
cmd,
argv[i],
));
continue;
}
let pid = mpid.unwrap() as pid_t;
if !find_wait_handles(WaitHandleQuery::Pid(pid), parser, &mut wait_handles) {
streams.err.append(wgettext_fmt!(
"%ls: Could not find a job with process id '%d'\n",
cmd,
pid,
));
}
} else {
// argument is process name
if !find_wait_handles(
WaitHandleQuery::ProcName(argv[i]),
parser,
&mut wait_handles,
) {
streams.err.append(wgettext_fmt!(
"%ls: Could not find child processes with the name '%ls'\n",
cmd,
argv[i],
));
}
}
}
if wait_handles.is_empty() {
return STATUS_INVALID_ARGS;
}
return wait_for_completion(parser, &wait_handles, any_flag);
}

View File

@ -1,422 +0,0 @@
use std::{array, cmp::Ordering};
use crate::{
wchar::{widestrs, wstr, WExt, WString, L},
wutil::sprintf,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Color24 {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Color24 {
fn from_bits(bits: u32) -> Self {
assert_eq!(bits >> 24, 0, "from_bits() called with non-zero high byte");
Self {
r: (bits >> 16) as u8,
g: (bits >> 8) as u8,
b: bits as u8,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Type {
// TODO: remove this? Users should probably use `Option<RgbColor>` instead
None,
Named { idx: u8 },
Rgb(Color24),
Normal,
Reset,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub struct Flags {
pub bold: bool,
pub underline: bool,
pub italics: bool,
pub dim: bool,
pub reverse: bool,
}
impl Flags {
// const eval workaround
const DEFAULT: Self = Flags {
bold: false,
underline: false,
italics: false,
dim: false,
reverse: false,
};
}
/// A type that represents a color.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct RgbColor {
pub typ: Type,
pub flags: Flags,
}
impl RgbColor {
/// The color white
pub const WHITE: Self = Self {
typ: Type::Named { idx: 7 },
flags: Flags::DEFAULT,
};
/// The color black
pub const BLACK: Self = Self {
typ: Type::Named { idx: 0 },
flags: Flags::DEFAULT,
};
/// The reset special color.
pub const RESET: Self = Self {
typ: Type::Reset,
flags: Flags::DEFAULT,
};
/// The normal special color.
pub const NORMAL: Self = Self {
typ: Type::Normal,
flags: Flags::DEFAULT,
};
/// The none special color.
pub const NONE: Self = Self {
typ: Type::None,
flags: Flags::DEFAULT,
};
/// Parse a color from a string.
pub fn from_wstr(s: &wstr) -> Option<Self> {
Self::try_parse_special(s)
.or_else(|| Self::try_parse_named(s))
.or_else(|| Self::try_parse_rgb(s))
}
/// Returns whether the color is the normal special color.
pub const fn is_normal(self) -> bool {
matches!(self.typ, Type::Normal)
}
/// Returns whether the color is the reset special color.
pub const fn is_reset(self) -> bool {
matches!(self.typ, Type::Reset)
}
/// Returns whether the color is the none special color.
pub const fn is_none(self) -> bool {
matches!(self.typ, Type::None)
}
/// Returns whether the color is a named color (like "magenta").
pub const fn is_named(self) -> bool {
matches!(self.typ, Type::Named { .. })
}
/// Returns whether the color is specified via RGB components.
pub const fn is_rgb(self) -> bool {
matches!(self.typ, Type::Rgb(_))
}
/// Returns whether the color is special, that is, not rgb or named.
pub const fn is_special(self) -> bool {
!self.is_named() && !self.is_rgb()
}
/// Returns a description of the color.
#[widestrs]
pub fn description(self) -> WString {
match self.typ {
Type::None => WString::from_str("none"),
Type::Named { idx } => {
sprintf!("named(%d, %ls)"L, idx, name_for_color_idx(idx).unwrap())
}
Type::Rgb(c) => {
sprintf!("rgb(0x%02x%02x%02x"L, c.r, c.g, c.b)
}
Type::Normal => WString::from_str("normal"),
Type::Reset => WString::from_str("reset"),
}
}
/// Returns the name index for the given color. Requires that the color be named or RGB.
pub fn to_name_index(self) -> u8 {
// TODO: This should look for the nearest color.
match self.typ {
Type::Named { idx } => idx,
Type::Rgb(c) => term16_color_for_rgb(c),
Type::None | Type::Normal | Type::Reset => {
panic!("to_name_index() called on Color that's not named or RGB")
}
}
}
/// Returns the term256 index for the given color. Requires that the color be RGB.
pub fn to_term256_index(self) -> u8 {
let Type::Rgb(c) = self.typ else {
panic!("Tried to get term256 index of non-RGB color");
};
term256_color_for_rgb(c)
}
/// Returns the 24 bit color for the given color. Requires that the color be RGB.
pub const fn to_color24(self) -> Color24 {
let Type::Rgb(c) = self.typ else {
panic!("Tried to get color24 of non-RGB color");
};
c
}
/// Returns the names of all named colors.
pub fn named_color_names() -> Vec<&'static wstr> {
let mut v: Vec<_> = NAMED_COLORS
.iter()
.filter_map(|&NamedColor { name, hidden, .. }| (!hidden).then_some(name))
.collect();
// "normal" isn't really a color and does not have a color palette index or
// RGB value. Therefore, it does not appear in the NAMED_COLORS table.
// However, it is a legitimate color name for the "set_color" command so
// include it in the publicly known list of colors. This is primarily so it
// appears in the output of "set_color --print-colors".
v.push(L!("normal"));
v
}
/// Try parsing a special color name like "normal".
#[widestrs]
fn try_parse_special(special: &wstr) -> Option<Self> {
// TODO: this is a very hot function, may need optimization by e.g. comparing length first,
// depending on how well inlining of `simple_icase_compare` works
let typ = if simple_icase_compare(special, "normal"L) == Ordering::Equal {
Type::Normal
} else if simple_icase_compare(special, "reset"L) == Ordering::Equal {
Type::Reset
} else {
return None;
};
Some(Self {
typ,
flags: Flags::default(),
})
}
/// Try parsing an rgb color like "#F0A030".
///
/// We support the following style of rgb formats (case insensitive):
///
/// - `#FA3`
/// - `#F3A035`
/// - `FA3`
/// - `F3A035`
fn try_parse_rgb(mut s: &wstr) -> Option<Self> {
// Skip any leading #.
if s.chars().next()? == '#' {
s = &s[1..];
}
let hex_digit = |i| {
s.char_at(i)
.to_digit(16)
.map(|n| n.try_into().expect("hex digit should always be < 256"))
};
// TODO: `array::try_from_fn()`: https://github.com/rust-lang/rust/issues/89379
let rgb: [_; 3] = if s.len() == 3 {
// Format: FA3
array::from_fn(hex_digit)
} else if s.len() == 6 {
// Format: F3A035
array::from_fn(|i| {
let hi = hex_digit(2 * i)?;
let lo = hex_digit(2 * i + 1)?;
Some(hi * 16 + lo)
})
} else {
return None;
};
Some(Self {
typ: Type::Rgb(Color24 {
r: rgb[0]?,
g: rgb[1]?,
b: rgb[2]?,
}),
flags: Flags::default(),
})
}
/// Try parsing an explicit color name like "magenta".
fn try_parse_named(name: &wstr) -> Option<Self> {
let i = NAMED_COLORS
.binary_search_by(|c| simple_icase_compare(c.name, name))
.ok()?;
Some(Self {
typ: Type::Named {
idx: NAMED_COLORS[i].idx,
},
flags: Flags::default(),
})
}
}
/// Compare wide strings with simple ASCII canonicalization.
#[inline(always)]
fn simple_icase_compare(s1: &wstr, s2: &wstr) -> Ordering {
let c1 = s1.chars().map(|c| c.to_ascii_lowercase());
let c2 = s2.chars().map(|c| c.to_ascii_lowercase());
c1.cmp(c2)
}
struct NamedColor {
name: &'static wstr,
idx: u8,
rgb: [u8; 3],
hidden: bool,
}
#[widestrs]
#[rustfmt::skip]
const NAMED_COLORS: &[NamedColor] = &[
// Keep this sorted alphabetically
NamedColor {name: "black"L, idx: 0, rgb: [0x00, 0x00, 0x00], hidden: false},
NamedColor {name: "blue"L, idx: 4, rgb: [0x00, 0x00, 0x80], hidden: false},
NamedColor {name: "brblack"L, idx: 8, rgb: [0x80, 0x80, 0x80], hidden: false},
NamedColor {name: "brblue"L, idx: 12, rgb: [0x00, 0x00, 0xFF], hidden: false},
NamedColor {name: "brbrown"L, idx: 11, rgb: [0xFF, 0xFF, 0x00], hidden: true},
NamedColor {name: "brcyan"L, idx: 14, rgb: [0x00, 0xFF, 0xFF], hidden: false},
NamedColor {name: "brgreen"L, idx: 10, rgb: [0x00, 0xFF, 0x00], hidden: false},
NamedColor {name: "brgrey"L, idx: 8, rgb: [0x55, 0x55, 0x55], hidden: true},
NamedColor {name: "brmagenta"L, idx: 13, rgb: [0xFF, 0x00, 0xFF], hidden: false},
NamedColor {name: "brown"L, idx: 3, rgb: [0x72, 0x50, 0x00], hidden: true},
NamedColor {name: "brpurple"L, idx: 13, rgb: [0xFF, 0x00, 0xFF], hidden: true},
NamedColor {name: "brred"L, idx: 9, rgb: [0xFF, 0x00, 0x00], hidden: false},
NamedColor {name: "brwhite"L, idx: 15, rgb: [0xFF, 0xFF, 0xFF], hidden: false},
NamedColor {name: "bryellow"L, idx: 11, rgb: [0xFF, 0xFF, 0x00], hidden: false},
NamedColor {name: "cyan"L, idx: 6, rgb: [0x00, 0x80, 0x80], hidden: false},
NamedColor {name: "green"L, idx: 2, rgb: [0x00, 0x80, 0x00], hidden: false},
NamedColor {name: "grey"L, idx: 7, rgb: [0xE5, 0xE5, 0xE5], hidden: true},
NamedColor {name: "magenta"L, idx: 5, rgb: [0x80, 0x00, 0x80], hidden: false},
NamedColor {name: "purple"L, idx: 5, rgb: [0x80, 0x00, 0x80], hidden: true},
NamedColor {name: "red"L, idx: 1, rgb: [0x80, 0x00, 0x00], hidden: false},
NamedColor {name: "white"L, idx: 7, rgb: [0xC0, 0xC0, 0xC0], hidden: false},
NamedColor {name: "yellow"L, idx: 3, rgb: [0x80, 0x80, 0x00], hidden: false},
];
assert_sorted_by_name!(NAMED_COLORS);
fn convert_color(color: Color24, colors: &[u32]) -> usize {
fn squared_difference(a: u8, b: u8) -> u16 {
u16::from(a.abs_diff(b)).pow(2)
}
colors
.iter()
.enumerate()
.min_by_key(|&(_i, c)| {
let Color24 { r, g, b } = Color24::from_bits(*c);
squared_difference(r, color.r)
+ squared_difference(g, color.g)
+ squared_difference(b, color.b)
})
.expect("convert_color() called with empty color list")
.0
}
fn name_for_color_idx(target_idx: u8) -> Option<&'static wstr> {
NAMED_COLORS
.iter()
.find_map(|&NamedColor { name, idx, .. }| (idx == target_idx).then_some(name))
}
fn term16_color_for_rgb(color: Color24) -> u8 {
const COLORS: &[u32] = &[
0x000000, // Black
0x800000, // Red
0x008000, // Green
0x808000, // Yellow
0x000080, // Blue
0x800080, // Magenta
0x008080, // Cyan
0xc0c0c0, // White
0x808080, // Bright Black
0xFF0000, // Bright Red
0x00FF00, // Bright Green
0xFFFF00, // Bright Yellow
0x0000FF, // Bright Blue
0xFF00FF, // Bright Magenta
0x00FFFF, // Bright Cyan
0xFFFFFF, // Bright White
];
convert_color(color, COLORS).try_into().unwrap()
}
fn term256_color_for_rgb(color: Color24) -> u8 {
const COLORS: &[u32] = &[
0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f, 0x005f87,
0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f, 0x008787, 0x0087af, 0x0087d7, 0x0087ff,
0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, 0x00d700, 0x00d75f, 0x00d787,
0x00d7af, 0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f, 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff,
0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f, 0x5f5f87,
0x5f5faf, 0x5f5fd7, 0x5f5fff, 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff,
0x5faf00, 0x5faf5f, 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787,
0x5fd7af, 0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff,
0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff, 0x875f00, 0x875f5f, 0x875f87,
0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x878787, 0x8787af, 0x8787d7, 0x8787ff,
0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, 0x87d700, 0x87d75f, 0x87d787,
0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f, 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff,
0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, 0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87,
0xaf5faf, 0xaf5fd7, 0xaf5fff, 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff,
0xafaf00, 0xafaf5f, 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f, 0xafd787,
0xafd7af, 0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff,
0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f, 0xd75f87,
0xd75faf, 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af, 0xd787d7, 0xd787ff,
0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff, 0xd7d700, 0xd7d75f, 0xd7d787,
0xd7d7af, 0xd7d7d7, 0xd7d7ff, 0xd7ff00, 0xd7ff5f, 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff,
0xff0000, 0xff005f, 0xff0087, 0xff00af, 0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f, 0xff5f87,
0xff5faf, 0xff5fd7, 0xff5fff, 0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff,
0xffaf00, 0xffaf5f, 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787,
0xffd7af, 0xffd7d7, 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff,
0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, 0x585858,
0x626262, 0x6c6c6c, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, 0xa8a8a8, 0xb2b2b2,
0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee,
];
(16 + convert_color(color, COLORS)).try_into().unwrap()
}
#[cfg(test)]
mod tests {
use crate::{color::RgbColor, wchar::widestrs};
#[test]
#[widestrs]
fn parse() {
assert!(RgbColor::from_wstr("#FF00A0"L).unwrap().is_rgb());
assert!(RgbColor::from_wstr("FF00A0"L).unwrap().is_rgb());
assert!(RgbColor::from_wstr("#F30"L).unwrap().is_rgb());
assert!(RgbColor::from_wstr("F30"L).unwrap().is_rgb());
assert!(RgbColor::from_wstr("f30"L).unwrap().is_rgb());
assert!(RgbColor::from_wstr("#FF30a5"L).unwrap().is_rgb());
assert!(RgbColor::from_wstr("3f30"L).is_none());
assert!(RgbColor::from_wstr("##f30"L).is_none());
assert!(RgbColor::from_wstr("magenta"L).unwrap().is_named());
assert!(RgbColor::from_wstr("MaGeNTa"L).unwrap().is_named());
assert!(RgbColor::from_wstr("mooganta"L).is_none());
}
}

View File

@ -1,354 +0,0 @@
use crate::ffi;
use crate::wchar::{wstr, WString};
use crate::wchar_ext::WExt;
use crate::wchar_ffi::c_str;
use crate::wchar_ffi::WCharFromFFI;
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};
use std::os::fd::AsRawFd;
use std::{ffi::c_uint, mem};
/// Like [`std::mem::replace()`] but provides a reference to the old value in a callback to obtain
/// the replacement value. Useful to avoid errors about multiple references (`&mut T` for `old` then
/// `&T` again in the `new` expression).
pub fn replace_with<T, F: FnOnce(&T) -> T>(old: &mut T, with: F) -> T {
let new = with(&*old);
std::mem::replace(old, new)
}
/// A RAII cleanup object. Unlike in C++ where there is no borrow checker, we can't just provide a
/// callback that modifies live objects willy-nilly because then there would be two &mut references
/// to the same object - the original variables we keep around to use and their captured references
/// held by the closure until its scope expires.
///
/// Instead we have a `ScopeGuard` type that takes exclusive ownership of (a mutable reference to)
/// the object to be managed. In lieu of keeping the original value around, we obtain a regular or
/// mutable reference to it via ScopeGuard's [`Deref`] and [`DerefMut`] impls.
///
/// The `ScopeGuard` is considered to be the exclusively owner of the passed value for the
/// duration of its lifetime. If you need to use the value again, use `ScopeGuard` to shadow the
/// value and obtain a reference to it via the `ScopeGuard` itself:
///
/// ```rust
/// use std::io::prelude::*;
///
/// let file = std::fs::File::open("/dev/null");
/// // Create a scope guard to write to the file when the scope expires.
/// // To be able to still use the file, shadow `file` with the ScopeGuard itself.
/// let mut file = ScopeGuard::new(file, |file| file.write_all(b"goodbye\n").unwrap());
/// // Now write to the file normally "through" the capturing ScopeGuard instance.
/// file.write_all(b"hello\n").unwrap();
///
/// // hello will be written first, then goodbye.
/// ```
pub struct ScopeGuard<T, F: FnOnce(&mut T)> {
captured: ManuallyDrop<T>,
on_drop: Option<F>,
}
impl<T, F: FnOnce(&mut T)> ScopeGuard<T, F> {
/// Creates a new `ScopeGuard` wrapping `value`. The `on_drop` callback is executed when the
/// ScopeGuard's lifetime expires or when it is manually dropped.
pub fn new(value: T, on_drop: F) -> Self {
Self {
captured: ManuallyDrop::new(value),
on_drop: Some(on_drop),
}
}
/// Cancel the unwind operation, e.g. do not call the previously passed-in `on_drop` callback
/// when the current scope expires.
pub fn cancel(guard: &mut Self) {
guard.on_drop.take();
}
/// Cancels the unwind operation like [`ScopeGuard::cancel()`] but also returns the captured
/// value (consuming the `ScopeGuard` in the process).
pub fn rollback(mut guard: Self) -> T {
guard.on_drop.take();
// Safety: we're about to forget the guard altogether
let value = unsafe { ManuallyDrop::take(&mut guard.captured) };
std::mem::forget(guard);
value
}
/// Commits the unwind operation (i.e. applies the provided callback) and returns the captured
/// value (consuming the `ScopeGuard` in the process).
pub fn commit(mut guard: Self) -> T {
(guard.on_drop.take().expect("ScopeGuard already canceled!"))(&mut guard.captured);
// Safety: we're about to forget the guard altogether
let value = unsafe { ManuallyDrop::take(&mut guard.captured) };
std::mem::forget(guard);
value
}
}
impl<T, F: FnOnce(&mut T)> Deref for ScopeGuard<T, F> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.captured
}
}
impl<T, F: FnOnce(&mut T)> DerefMut for ScopeGuard<T, F> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.captured
}
}
impl<T, F: FnOnce(&mut T)> Drop for ScopeGuard<T, F> {
fn drop(&mut self) {
if let Some(on_drop) = self.on_drop.take() {
on_drop(&mut self.captured);
}
// Safety: we're in the Drop so `self` will never be accessed again.
unsafe { ManuallyDrop::drop(&mut self.captured) };
}
}
/// A scoped manager to save the current value of some variable, and optionally set it to a new
/// value. When dropped, it restores the variable to its old value.
///
/// This can be handy when there are multiple code paths to exit a block. Note that this can only be
/// used if the code does not access the captured variable again for the duration of the scope. If
/// that's not the case (the code will refuse to compile), use a [`ScopeGuard`] instance instead.
pub struct ScopedPush<'a, T> {
var: &'a mut T,
saved_value: Option<T>,
}
impl<'a, T> ScopedPush<'a, T> {
pub fn new(var: &'a mut T, new_value: T) -> Self {
let saved_value = mem::replace(var, new_value);
Self {
var,
saved_value: Some(saved_value),
}
}
pub fn restore(&mut self) {
if let Some(saved_value) = self.saved_value.take() {
*self.var = saved_value;
}
}
}
impl<'a, T> Drop for ScopedPush<'a, T> {
fn drop(&mut self) {
self.restore()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EscapeStringStyle {
Script(EscapeFlags),
Url,
Var,
Regex,
}
/// Flags for the [`escape_string()`] function. These are only applicable when the escape style is
/// [`EscapeStringStyle::Script`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EscapeFlags {
/// Do not escape special fish syntax characters like the semicolon. Only escape non-printable
/// characters and backslashes.
pub no_printables: bool,
/// Do not try to use 'simplified' quoted escapes, and do not use empty quotes as the empty
/// string.
pub no_quoted: bool,
/// Do not escape tildes.
pub no_tilde: bool,
/// Replace non-printable control characters with Unicode symbols.
pub symbolic: bool,
}
/// Replace special characters with backslash escape sequences. Newline is replaced with `\n`, etc.
pub fn escape_string(s: &wstr, style: EscapeStringStyle) -> WString {
let mut flags_int = 0;
let style = match style {
EscapeStringStyle::Script(flags) => {
const ESCAPE_NO_PRINTABLES: c_uint = 1 << 0;
const ESCAPE_NO_QUOTED: c_uint = 1 << 1;
const ESCAPE_NO_TILDE: c_uint = 1 << 2;
const ESCAPE_SYMBOLIC: c_uint = 1 << 3;
if flags.no_printables {
flags_int |= ESCAPE_NO_PRINTABLES;
}
if flags.no_quoted {
flags_int |= ESCAPE_NO_QUOTED;
}
if flags.no_tilde {
flags_int |= ESCAPE_NO_TILDE;
}
if flags.symbolic {
flags_int |= ESCAPE_SYMBOLIC;
}
ffi::escape_string_style_t::STRING_STYLE_SCRIPT
}
EscapeStringStyle::Url => ffi::escape_string_style_t::STRING_STYLE_URL,
EscapeStringStyle::Var => ffi::escape_string_style_t::STRING_STYLE_VAR,
EscapeStringStyle::Regex => ffi::escape_string_style_t::STRING_STYLE_REGEX,
};
ffi::escape_string(c_str!(s), flags_int.into(), style).from_ffi()
}
/// Test if the string is a valid function name.
pub fn valid_func_name(name: &wstr) -> bool {
if name.is_empty() {
return false;
};
if name.char_at(0) == '-' {
return false;
};
// A function name needs to be a valid path, so no / and no NULL.
if name.find_char('/').is_some() {
return false;
};
if name.find_char('\0').is_some() {
return false;
};
true
}
pub const fn assert_send<T: Send>() {}
pub const fn assert_sync<T: Sync>() {}
/// A rusty port of the C++ `write_loop()` function from `common.cpp`. This should be deprecated in
/// favor of native rust read/write methods at some point.
///
/// Returns the number of bytes written or an IO error.
pub fn write_loop<Fd: AsRawFd>(fd: &Fd, buf: &[u8]) -> std::io::Result<usize> {
let fd = fd.as_raw_fd();
let mut total = 0;
while total < buf.len() {
let written =
unsafe { libc::write(fd, buf[total..].as_ptr() as *const _, buf.len() - total) };
if written < 0 {
let errno = errno::errno().0;
if matches!(errno, libc::EAGAIN | libc::EINTR) {
continue;
}
return Err(std::io::Error::from_raw_os_error(errno));
}
total += written as usize;
}
Ok(total)
}
/// A rusty port of the C++ `read_loop()` function from `common.cpp`. This should be deprecated in
/// favor of native rust read/write methods at some point.
///
/// Returns the number of bytes read or an IO error.
pub fn read_loop<Fd: AsRawFd>(fd: &Fd, buf: &mut [u8]) -> std::io::Result<usize> {
let fd = fd.as_raw_fd();
loop {
let read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
if read < 0 {
let errno = errno::errno().0;
if matches!(errno, libc::EAGAIN | libc::EINTR) {
continue;
}
return Err(std::io::Error::from_raw_os_error(errno));
}
return Ok(read as usize);
}
}
/// Asserts that a slice is alphabetically sorted by a [`&wstr`] `name` field.
///
/// Mainly useful for static asserts/const eval.
///
/// # Panics
///
/// This function panics if the given slice is unsorted.
///
/// # Examples
///
/// ```rust
/// const COLORS: &[(&wstr, u32)] = &[
/// // must be in alphabetical order
/// (L!("blue"), 0x0000ff),
/// (L!("green"), 0x00ff00),
/// (L!("red"), 0xff0000),
/// ];
///
/// assert_sorted_by_name!(COLORS, 0);
/// ```
macro_rules! assert_sorted_by_name {
($slice:expr, $field:tt) => {
const _: () = {
use std::cmp::Ordering;
// ugly const eval workarounds below.
const fn cmp_i32(lhs: i32, rhs: i32) -> Ordering {
match lhs - rhs {
..=-1 => Ordering::Less,
0 => Ordering::Equal,
1.. => Ordering::Greater,
}
}
const fn cmp_slice(s1: &[char], s2: &[char]) -> Ordering {
let mut i = 0;
while i < s1.len() && i < s2.len() {
match cmp_i32(s1[i] as i32, s2[i] as i32) {
Ordering::Equal => i += 1,
other => return other,
}
}
cmp_i32(s1.len() as i32, s2.len() as i32)
}
let mut i = 1;
while i < $slice.len() {
let prev = $slice[i - 1].$field.as_char_slice();
let cur = $slice[i].$field.as_char_slice();
if matches!(cmp_slice(prev, cur), Ordering::Greater) {
panic!("array must be sorted");
}
i += 1;
}
};
};
($slice:expr) => {
assert_sorted_by_name!($slice, name);
};
}
mod tests {
use crate::{
common::{escape_string, EscapeStringStyle},
wchar::widestrs,
};
#[widestrs]
pub fn test_escape_string() {
let regex = |input| escape_string(input, EscapeStringStyle::Regex);
// plain text should not be needlessly escaped
assert_eq!(regex("hello world!"L), "hello world!"L);
// all the following are intended to be ultimately matched literally - even if they don't look
// like that's the intent - so we escape them.
assert_eq!(regex(".ext"L), "\\.ext"L);
assert_eq!(regex("{word}"L), "\\{word\\}"L);
assert_eq!(regex("hola-mundo"L), "hola\\-mundo"L);
assert_eq!(
regex("$17.42 is your total?"L),
"\\$17\\.42 is your total\\?"L
);
assert_eq!(
regex("not really escaped\\?"L),
"not really escaped\\\\\\?"L
);
}
}
crate::ffi_tests::add_test!("escape_string", tests::test_escape_string);

View File

@ -1,50 +0,0 @@
/// Flags that may be passed as the 'mode' in env_stack_t::set() / environment_t::get().
pub mod flags {
use autocxx::c_int;
use bitflags::bitflags;
bitflags! {
/// Flags that may be passed as the 'mode' in env_stack_t::set() / environment_t::get().
#[repr(C)]
pub struct EnvMode: u16 {
/// Default mode. Used with `env_stack_t::get()` to indicate the caller doesn't care what scope
/// the var is in or whether it is exported or unexported.
const DEFAULT = 0;
/// Flag for local (to the current block) variable.
const LOCAL = 1 << 0;
const FUNCTION = 1 << 1;
/// Flag for global variable.
const GLOBAL = 1 << 2;
/// Flag for universal variable.
const UNIVERSAL = 1 << 3;
/// Flag for exported (to commands) variable.
const EXPORT = 1 << 4;
/// Flag for unexported variable.
const UNEXPORT = 1 << 5;
/// Flag to mark a variable as a path variable.
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 From<EnvMode> for c_int {
fn from(val: EnvMode) -> Self {
c_int(i32::from(val.bits()))
}
}
}
/// Return values for `env_stack_t::set()`.
pub mod status {
pub const ENV_OK: i32 = 0;
pub const ENV_PERM: i32 = 1;
pub const ENV_SCOPE: i32 = 2;
pub const ENV_INVALID: i32 = 3;
pub const ENV_NOT_FOUND: i32 = 4;
}

View File

@ -1,929 +0,0 @@
//! Functions for handling event triggers
//!
//! Because most of these functions can be called by signal handler, it is important to make it well
//! defined when these functions produce output or perform memory allocations, since such functions
//! may not be safely called by signal handlers.
use autocxx::WithinUniquePtr;
use cxx::{CxxVector, CxxWString, UniquePtr};
use libc::pid_t;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use widestring_suffix::widestrs;
use crate::builtins::shared::io_streams_t;
use crate::common::{escape_string, replace_with, EscapeFlags, EscapeStringStyle, ScopeGuard};
use crate::ffi::{
self, block_t, parser_t, signal_check_cancel, signal_handle, termsize_container_t, Repin,
};
use crate::flog::FLOG;
use crate::signal::{sig2wcs, signal_get_desc};
use crate::wchar::{wstr, WString, L};
use crate::wchar_ffi::{wcharz_t, AsWstr, WCharFromFFI, WCharToFFI};
use crate::wutil::sprintf;
#[cxx::bridge]
mod event_ffi {
extern "C++" {
include!("wutil.h");
include!("parser.h");
include!("io.h");
type wcharz_t = crate::ffi::wcharz_t;
type parser_t = crate::ffi::parser_t;
type io_streams_t = crate::ffi::io_streams_t;
}
enum event_type_t {
any,
signal,
variable,
process_exit,
job_exit,
caller_exit,
generic,
}
struct event_description_t {
typ: event_type_t,
signal: i32,
pid: i32,
internal_job_id: u64,
caller_id: u64,
str_param1: UniquePtr<CxxWString>,
}
extern "Rust" {
type EventHandler;
type Event;
fn new_event_generic(desc: wcharz_t) -> Box<Event>;
fn new_event_variable_erase(name: &CxxWString) -> Box<Event>;
fn new_event_variable_set(name: &CxxWString) -> Box<Event>;
fn new_event_process_exit(pid: i32, status: i32) -> Box<Event>;
fn new_event_job_exit(pgid: i32, jid: u64) -> Box<Event>;
fn new_event_caller_exit(internal_job_id: u64, job_id: i32) -> Box<Event>;
#[cxx_name = "clone"]
fn clone_ffi(self: &Event) -> Box<Event>;
#[cxx_name = "event_add_handler"]
fn event_add_handler_ffi(desc: &event_description_t, name: &CxxWString);
#[cxx_name = "event_remove_function_handlers"]
fn event_remove_function_handlers_ffi(name: &CxxWString) -> usize;
#[cxx_name = "event_get_function_handler_descs"]
fn event_get_function_handler_descs_ffi(name: &CxxWString) -> Vec<event_description_t>;
fn desc(self: &EventHandler) -> event_description_t;
fn function_name(self: &EventHandler) -> UniquePtr<CxxWString>;
fn set_removed(self: &mut EventHandler);
fn event_fire_generic_ffi(
parser: Pin<&mut parser_t>,
name: &CxxWString,
arguments: &CxxVector<wcharz_t>,
);
#[cxx_name = "event_get_desc"]
fn event_get_desc_ffi(parser: &parser_t, evt: &Event) -> UniquePtr<CxxWString>;
#[cxx_name = "event_fire_delayed"]
fn event_fire_delayed_ffi(parser: Pin<&mut parser_t>);
#[cxx_name = "event_fire"]
fn event_fire_ffi(parser: Pin<&mut parser_t>, event: &Event);
#[cxx_name = "event_print"]
fn event_print_ffi(streams: Pin<&mut io_streams_t>, type_filter: &CxxWString);
#[cxx_name = "event_enqueue_signal"]
fn enqueue_signal(signal: usize);
#[cxx_name = "event_is_signal_observed"]
fn is_signal_observed(sig: usize) -> bool;
}
}
pub use event_ffi::{event_description_t, event_type_t};
const ANY_PID: pid_t = 0;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum EventType {
/// Matches any event type (not always any event, as the function name may limit the choice as
/// well).
Any,
/// An event triggered by a signal.
Signal { signal: i32 },
/// An event triggered by a variable update.
Variable { name: WString },
/// An event triggered by a process exit.
ProcessExit {
/// Process ID. Use [`ANY_PID`] to match any pid.
pid: pid_t,
},
/// An event triggered by a job exit.
JobExit {
/// pid requested by the event, or [`ANY_PID`] for all.
pid: pid_t,
/// `internal_job_id` of the job to match.
/// If this is 0, we match either all jobs (`pid == ANY_PID`) or no jobs (otherwise).
internal_job_id: u64,
},
/// An event triggered by a job exit, triggering the 'caller'-style events only.
CallerExit {
/// Internal job ID.
caller_id: u64,
},
/// A generic event.
Generic {
/// The parameter describing this generic event.
param: WString,
},
}
impl EventType {
fn str_param1(&self) -> Option<&wstr> {
match self {
EventType::Any
| EventType::Signal { .. }
| EventType::ProcessExit { .. }
| EventType::JobExit { .. }
| EventType::CallerExit { .. } => None,
EventType::Variable { name } => Some(name),
EventType::Generic { param } => Some(param),
}
}
#[widestrs]
fn name(&self) -> &'static wstr {
match self {
EventType::Any => "any"L,
EventType::Signal { .. } => "signal"L,
EventType::Variable { .. } => "variable"L,
EventType::ProcessExit { .. } => "process-exit"L,
EventType::JobExit { .. } => "job-exit"L,
EventType::CallerExit { .. } => "caller-exit"L,
EventType::Generic { .. } => "generic"L,
}
}
fn matches_filter(&self, filter: &wstr) -> bool {
if filter.is_empty() {
return true;
}
match self {
EventType::Any => false,
EventType::ProcessExit { .. }
| EventType::JobExit { .. }
| EventType::CallerExit { .. }
if filter == L!("exit") =>
{
true
}
_ => filter == self.name(),
}
}
}
impl From<&EventType> for event_type_t {
fn from(typ: &EventType) -> Self {
match typ {
EventType::Any => event_type_t::any,
EventType::Signal { .. } => event_type_t::signal,
EventType::Variable { .. } => event_type_t::variable,
EventType::ProcessExit { .. } => event_type_t::process_exit,
EventType::JobExit { .. } => event_type_t::job_exit,
EventType::CallerExit { .. } => event_type_t::caller_exit,
EventType::Generic { .. } => event_type_t::generic,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventDescription {
// TODO: remove the wrapper struct and just put `EventType` where `EventDescription` is now
typ: EventType,
}
impl From<&event_description_t> for EventDescription {
fn from(desc: &event_description_t) -> Self {
EventDescription {
typ: match desc.typ {
event_type_t::any => EventType::Any,
event_type_t::signal => EventType::Signal {
signal: desc.signal,
},
event_type_t::variable => EventType::Variable {
name: desc.str_param1.from_ffi(),
},
event_type_t::process_exit => EventType::ProcessExit { pid: desc.pid },
event_type_t::job_exit => EventType::JobExit {
pid: desc.pid,
internal_job_id: desc.internal_job_id,
},
event_type_t::caller_exit => EventType::CallerExit {
caller_id: desc.caller_id,
},
event_type_t::generic => EventType::Generic {
param: desc.str_param1.from_ffi(),
},
_ => panic!("invalid event description"),
},
}
}
}
impl From<&EventDescription> for event_description_t {
fn from(desc: &EventDescription) -> Self {
let mut result = event_description_t {
typ: (&desc.typ).into(),
signal: Default::default(),
pid: Default::default(),
internal_job_id: Default::default(),
caller_id: Default::default(),
str_param1: match desc.typ.str_param1() {
Some(param) => param.to_ffi(),
None => UniquePtr::null(),
},
};
match desc.typ {
EventType::Any => (),
EventType::Signal { signal } => result.signal = signal,
EventType::Variable { .. } => (),
EventType::ProcessExit { pid } => result.pid = pid,
EventType::JobExit {
pid,
internal_job_id,
} => {
result.pid = pid;
result.internal_job_id = internal_job_id;
}
EventType::CallerExit { caller_id } => result.caller_id = caller_id,
EventType::Generic { .. } => (),
}
result
}
}
#[derive(Debug)]
pub struct EventHandler {
/// Properties of the event to match.
desc: EventDescription,
/// Name of the function to invoke.
function_name: WString,
/// A flag set when an event handler is removed from the global list.
/// Once set, this is never cleared.
removed: AtomicBool,
/// A flag set when an event handler is first fired.
fired: AtomicBool,
}
impl EventHandler {
pub fn new(desc: EventDescription, name: Option<WString>) -> Self {
Self {
desc,
function_name: name.unwrap_or_else(WString::new),
removed: AtomicBool::new(false),
fired: AtomicBool::new(false),
}
}
/// \return true if a handler is "one shot": it fires at most once.
fn is_one_shot(&self) -> bool {
match self.desc.typ {
EventType::ProcessExit { pid } => pid != ANY_PID,
EventType::JobExit { pid, .. } => pid != ANY_PID,
EventType::CallerExit { .. } => true,
EventType::Signal { .. }
| EventType::Variable { .. }
| EventType::Generic { .. }
| EventType::Any => false,
}
}
/// Tests if this event handler matches an event that has occurred.
fn matches(&self, event: &Event) -> bool {
match (&self.desc.typ, &event.desc.typ) {
(EventType::Any, _) => true,
(EventType::Signal { signal }, EventType::Signal { signal: ev_signal }) => {
signal == ev_signal
}
(EventType::Variable { name }, EventType::Variable { name: ev_name }) => {
name == ev_name
}
(EventType::ProcessExit { pid }, EventType::ProcessExit { pid: ev_pid }) => {
*pid == ANY_PID || pid == ev_pid
}
(
EventType::JobExit {
pid,
internal_job_id,
},
EventType::JobExit {
internal_job_id: ev_internal_job_id,
..
},
) => *pid == ANY_PID || internal_job_id == ev_internal_job_id,
(
EventType::CallerExit { caller_id },
EventType::CallerExit {
caller_id: ev_caller_id,
},
) => caller_id == ev_caller_id,
(EventType::Generic { param }, EventType::Generic { param: ev_param }) => {
param == ev_param
}
(_, _) => false,
}
}
}
type EventHandlerList = Vec<Arc<EventHandler>>;
impl EventHandler {
fn desc(&self) -> event_description_t {
(&self.desc).into()
}
fn function_name(self: &EventHandler) -> UniquePtr<CxxWString> {
self.function_name.to_ffi()
}
fn set_removed(self: &mut EventHandler) {
self.removed.store(true, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
desc: EventDescription,
arguments: Vec<WString>,
}
impl Event {
pub fn generic(desc: WString) -> Self {
Self {
desc: EventDescription {
typ: EventType::Generic { param: desc },
},
arguments: vec![],
}
}
pub fn variable_erase(name: WString) -> Self {
Self {
desc: EventDescription {
typ: EventType::Variable { name: name.clone() },
},
arguments: vec!["VARIABLE".into(), "ERASE".into(), name],
}
}
pub fn variable_set(name: WString) -> Self {
Self {
desc: EventDescription {
typ: EventType::Variable { name: name.clone() },
},
arguments: vec!["VARIABLE".into(), "SET".into(), name],
}
}
pub fn process_exit(pid: pid_t, status: i32) -> Self {
Self {
desc: EventDescription {
typ: EventType::ProcessExit { pid },
},
arguments: vec![
"PROCESS_EXIT".into(),
pid.to_string().into(),
status.to_string().into(),
],
}
}
pub fn job_exit(pgid: pid_t, jid: u64) -> Self {
Self {
desc: EventDescription {
typ: EventType::JobExit {
pid: pgid,
internal_job_id: jid,
},
},
arguments: vec![
"JOB_EXIT".into(),
pgid.to_string().into(),
"0".into(), // historical
],
}
}
pub fn caller_exit(internal_job_id: u64, job_id: i32) -> Self {
Self {
desc: EventDescription {
typ: EventType::CallerExit {
caller_id: internal_job_id,
},
},
arguments: vec![
"JOB_EXIT".into(),
job_id.to_string().into(),
"0".into(), // historical
],
}
}
/// Test if specified event is blocked.
fn is_blocked(&self, parser: &mut parser_t) -> bool {
let mut i = 0;
while let Some(block) = parser.get_block_at_index(i) {
i += 1;
if block.ffi_event_blocks() != 0 {
return true;
}
}
parser.ffi_global_event_blocks() != 0
}
}
fn new_event_generic(desc: wcharz_t) -> Box<Event> {
Box::new(Event::generic(desc.into()))
}
fn new_event_variable_erase(name: &CxxWString) -> Box<Event> {
Box::new(Event::variable_erase(name.from_ffi()))
}
fn new_event_variable_set(name: &CxxWString) -> Box<Event> {
Box::new(Event::variable_set(name.from_ffi()))
}
fn new_event_process_exit(pid: i32, status: i32) -> Box<Event> {
Box::new(Event::process_exit(pid, status))
}
fn new_event_job_exit(pgid: i32, jid: u64) -> Box<Event> {
Box::new(Event::job_exit(pgid, jid))
}
fn new_event_caller_exit(internal_job_id: u64, job_id: i32) -> Box<Event> {
Box::new(Event::caller_exit(internal_job_id, job_id))
}
impl Event {
fn clone_ffi(&self) -> Box<Event> {
Box::new(self.clone())
}
}
fn event_add_handler_ffi(desc: &event_description_t, name: &CxxWString) {
add_handler(EventHandler::new(desc.into(), Some(name.from_ffi())));
}
/// All the signals we are interested in are in the 1-32 range (with 32 being the typical SIGRTMAX),
/// but we can expand it to 64 just to be safe. All code checks if a signal value is within bounds
/// before handling it.
const SIGNAL_COUNT: usize = 64;
struct PendingSignals {
/// A counter that is incremented each time a pending signal is received.
counter: AtomicU32,
/// List of pending signals.
received: [AtomicBool; SIGNAL_COUNT],
/// The last counter visible in `acquire_pending()`.
/// This is not accessed from a signal handler.
last_counter: Mutex<u32>,
}
impl PendingSignals {
/// Mark a signal as pending. This may be called from a signal handler. We expect only one
/// signal handler to execute at once. Also note that these may be coalesced.
pub fn mark(&self, which: usize) {
if let Some(received) = self.received.get(which) {
received.store(true, Ordering::Relaxed);
self.counter.fetch_add(1, Ordering::Relaxed);
}
}
/// Return the list of signals that were set as the bits in a u64, clearing them.
pub fn acquire_pending(&self) -> u64 {
let mut current = self
.last_counter
.lock()
.expect("mutex should not be poisoned");
// Check the counter first. If it hasn't changed, no signals have been received.
let count = self.counter.load(Ordering::Acquire);
if count == *current {
return 0;
}
// The signal count has changed. Store the new counter and fetch all set signals.
*current = count;
let mut result = 0;
for (i, received) in self.received.iter().enumerate() {
if received.load(Ordering::Relaxed) {
result |= 1_u64 << i;
received.store(false, Ordering::Relaxed);
}
}
result
}
}
// Required until inline const is stabilized.
#[allow(clippy::declare_interior_mutable_const)]
const ATOMIC_BOOL_FALSE: AtomicBool = AtomicBool::new(false);
#[allow(clippy::declare_interior_mutable_const)]
const ATOMIC_U32_0: AtomicU32 = AtomicU32::new(0);
static PENDING_SIGNALS: PendingSignals = PendingSignals {
counter: AtomicU32::new(0),
received: [ATOMIC_BOOL_FALSE; SIGNAL_COUNT],
last_counter: Mutex::new(0),
};
/// List of event handlers. **While this is locked to allow safely accessing/modifying the vector,
/// note that it does NOT provide exclusive access to the [`EventHandler`] objects which are shared
/// references (in an `Arc<T>`).**
static EVENT_HANDLERS: Mutex<EventHandlerList> = Mutex::new(Vec::new());
/// Tracks the number of registered event handlers for each signal.
/// This is inspected by a signal handler. We assume no values in here overflow.
static OBSERVED_SIGNALS: [AtomicU32; SIGNAL_COUNT] = [ATOMIC_U32_0; SIGNAL_COUNT];
/// List of events that have been sent but have not yet been delivered because they are blocked.
///
/// This was part of profile_item_t accessed as parser.libdata().blocked_events and has been
/// temporarily moved here. There was no mutex around this in the cpp code. TODO: Move it back.
static BLOCKED_EVENTS: Mutex<Vec<Event>> = Mutex::new(Vec::new());
fn inc_signal_observed(sig: i32) {
if let Ok(index) = usize::try_from(sig) {
if let Some(sig) = OBSERVED_SIGNALS.get(index) {
sig.fetch_add(1, Ordering::Relaxed);
}
}
}
fn dec_signal_observed(sig: i32) {
if let Ok(index) = usize::try_from(sig) {
if let Some(sig) = OBSERVED_SIGNALS.get(index) {
sig.fetch_sub(1, Ordering::Relaxed);
}
}
}
/// Returns whether an event listener is registered for the given signal. This is safe to call from
/// a signal handler.
pub fn is_signal_observed(sig: usize) -> bool {
// We are in a signal handler!
OBSERVED_SIGNALS
.get(sig)
.map_or(false, |s| s.load(Ordering::Relaxed) > 0)
}
pub fn get_desc(parser: &parser_t, evt: &Event) -> WString {
let s = match &evt.desc.typ {
EventType::Signal { signal } => format!(
"signal handler for {} ({})",
sig2wcs(*signal),
signal_get_desc(*signal)
),
EventType::Variable { name } => format!("handler for variable '{name}'"),
EventType::ProcessExit { pid } => format!("exit handler for process {pid}"),
EventType::JobExit { pid, .. } => {
if let Some(job) = parser.job_get_from_pid(*pid) {
format!(
"exit handler for job {}, '{}'",
job.job_id().0,
job.command()
)
} else {
format!("exit handler for job with pid {pid}")
}
}
EventType::CallerExit { .. } => "exit handler for command substitution caller".to_string(),
EventType::Generic { param } => format!("handler for generic event '{param}'"),
EventType::Any => unreachable!(),
};
WString::from_str(&s)
}
fn event_get_desc_ffi(parser: &parser_t, evt: &Event) -> UniquePtr<CxxWString> {
get_desc(parser, evt).to_ffi()
}
/// Add an event handler.
pub fn add_handler(eh: EventHandler) {
if let EventType::Signal { signal } = eh.desc.typ {
signal_handle(ffi::c_int(signal));
inc_signal_observed(signal);
}
EVENT_HANDLERS
.lock()
.expect("event handler list should not be poisoned")
.push(Arc::new(eh));
}
/// Remove handlers where `pred` returns true. Simultaneously update our `signal_observed` array.
fn remove_handlers_if(pred: impl Fn(&EventHandler) -> bool) -> usize {
let mut handlers = EVENT_HANDLERS
.lock()
.expect("event handler list should not be poisoned");
let mut removed = 0;
for i in (0..handlers.len()).rev() {
let handler = &handlers[i];
if pred(handler) {
handler.removed.store(true, Ordering::Relaxed);
if let EventType::Signal { signal } = handler.desc.typ {
dec_signal_observed(signal);
}
handlers.remove(i);
removed += 1;
}
}
removed
}
/// Remove all events for the given function name.
pub fn remove_function_handlers(name: &wstr) -> usize {
remove_handlers_if(|h| h.function_name == name)
}
fn event_remove_function_handlers_ffi(name: &CxxWString) -> usize {
remove_function_handlers(name.as_wstr())
}
/// Return all event handlers for the given function.
pub fn get_function_handlers(name: &wstr) -> EventHandlerList {
EVENT_HANDLERS
.lock()
.expect("event handler list should not be poisoned")
.iter()
.filter(|h| h.function_name == name)
.cloned()
.collect()
}
fn event_get_function_handler_descs_ffi(name: &CxxWString) -> Vec<event_description_t> {
get_function_handlers(name.as_wstr())
.iter()
.map(|h| event_description_t::from(&h.desc))
.collect()
}
/// Perform the specified event. Since almost all event firings will not be matched by even a single
/// event handler, we make sure to optimize the 'no matches' path. This means that nothing is
/// allocated/initialized unless needed.
fn fire_internal(parser: &mut parser_t, event: &Event) {
assert!(
parser.libdata_pod().is_event >= 0,
"is_event should not be negative"
);
// Suppress fish_trace during events.
let saved_is_event = replace_with(&mut parser.libdata_pod().is_event, |old| old + 1);
let saved_suppress_fish_trace =
std::mem::replace(&mut parser.libdata_pod().suppress_fish_trace, true);
let mut parser = ScopeGuard::new(parser, |parser| {
parser.libdata_pod().is_event = saved_is_event;
parser.libdata_pod().suppress_fish_trace = saved_suppress_fish_trace;
});
// Capture the event handlers that match this event.
let fire: Vec<_> = EVENT_HANDLERS
.lock()
.expect("event handler list should not be poisoned")
.iter()
.filter(|h| h.matches(event))
.cloned()
.collect();
// Iterate over our list of matching events. Fire the ones that are still present.
let mut fired_one_shot = false;
for handler in fire {
// A previous handler may have erased this one.
if handler.removed.load(Ordering::Relaxed) {
continue;
};
// Construct a buffer to evaluate, starting with the function name and then all the
// arguments.
let mut buffer = handler.function_name.clone();
for arg in &event.arguments {
buffer.push(' ');
buffer.push_utfstr(&escape_string(
arg,
EscapeStringStyle::Script(EscapeFlags::default()),
));
}
// Event handlers are not part of the main flow of code, so they are marked as
// non-interactive.
let saved_is_interactive =
std::mem::replace(&mut parser.libdata_pod().is_interactive, false);
let saved_statuses = parser.get_last_statuses().within_unique_ptr();
let mut parser = ScopeGuard::new(&mut parser, |parser| {
parser.pin().set_last_statuses(saved_statuses);
parser.libdata_pod().is_interactive = saved_is_interactive;
});
FLOG!(
event,
"Firing event '",
event.desc.typ.str_param1().unwrap_or(L!("")),
"' to handler '",
handler.function_name,
"'"
);
let b = parser
.pin()
.push_block(block_t::event_block((event as *const Event).cast()).within_unique_ptr());
parser
.pin()
.eval_string_ffi1(&buffer.to_ffi())
.within_unique_ptr();
parser.pin().pop_block(b);
handler.fired.store(true, Ordering::Relaxed);
fired_one_shot |= handler.is_one_shot();
}
if fired_one_shot {
remove_handlers_if(|h| h.fired.load(Ordering::Relaxed) && h.is_one_shot());
}
}
/// Fire all delayed events attached to the given parser.
pub fn fire_delayed(parser: &mut parser_t) {
let ld = parser.libdata_pod();
// Do not invoke new event handlers from within event handlers.
if ld.is_event != 0 {
return;
};
// Do not invoke new event handlers if we are unwinding (#6649).
if signal_check_cancel().0 != 0 {
return;
};
// We unfortunately can't keep this locked until we're done with it because the SIGWINCH handler
// code might call back into here and we would delay processing of the events, leading to a test
// failure under CI. (Yes, the `&mut parser_t` is a lie.)
let mut to_send = std::mem::take(&mut *BLOCKED_EVENTS.lock().expect("Mutex poisoned!"));
// Append all signal events to to_send.
// 'signals' contains a bit set for each signal that has been received.
let mut signals: u64 = PENDING_SIGNALS.acquire_pending();
while signals != 0 {
let sig = signals.trailing_zeros();
signals &= !(1_u64 << sig);
let sig = sig as i32;
// HACK: The only variables we change in response to a *signal* are $COLUMNS and $LINES.
// Do that now.
if sig == libc::SIGWINCH {
termsize_container_t::ffi_updating(parser.pin()).within_unique_ptr();
}
let event = Event {
desc: EventDescription {
typ: EventType::Signal { signal: sig },
},
arguments: vec![sig2wcs(sig).into()],
};
to_send.push(event);
}
// Fire or re-block all events. Don't obtain BLOCKED_EVENTS until we know that we have at least
// one event that is blocked.
let mut blocked_events = None;
for event in to_send {
if event.is_blocked(parser) {
if blocked_events.is_none() {
blocked_events = Some(BLOCKED_EVENTS.lock().expect("Mutex posioned"));
}
blocked_events.as_mut().unwrap().push(event);
} else {
// fire_internal() does not access BLOCKED_EVENTS so this call can't deadlock.
fire_internal(parser, &event);
}
}
}
fn event_fire_delayed_ffi(parser: Pin<&mut parser_t>) {
fire_delayed(parser.unpin())
}
/// Enqueue a signal event. Invoked from a signal handler.
pub fn enqueue_signal(signal: usize) {
// Beware, we are in a signal handler
PENDING_SIGNALS.mark(signal);
}
/// Fire the specified event event, executing it on `parser`.
pub fn fire(parser: &mut parser_t, event: Event) {
// Fire events triggered by signals.
fire_delayed(parser);
if event.is_blocked(parser) {
BLOCKED_EVENTS.lock().expect("Mutex poisoned!").push(event);
} else {
fire_internal(parser, &event);
}
}
fn event_fire_ffi(parser: Pin<&mut parser_t>, event: &Event) {
fire(parser.unpin(), event.clone())
}
#[widestrs]
const EVENT_FILTER_NAMES: [&wstr; 7] = [
"signal"L,
"variable"L,
"exit"L,
"process-exit"L,
"job-exit"L,
"caller-exit"L,
"generic"L,
];
/// Print all events. If type_filter is not empty, only output events with that type.
pub fn print(streams: &mut io_streams_t, type_filter: &wstr) {
let mut tmp = EVENT_HANDLERS
.lock()
.expect("event handler list should not be poisoned")
.clone();
tmp.sort_by(|e1, e2| e1.desc.typ.cmp(&e2.desc.typ));
let mut last_type = None;
for evt in tmp {
// If we have a filter, skip events that don't match.
if !evt.desc.typ.matches_filter(type_filter) {
continue;
}
if last_type.as_ref() != Some(&evt.desc.typ) {
if last_type.is_some() {
streams.out.append(L!("\n"));
}
last_type = Some(evt.desc.typ.clone());
streams
.out
.append(&sprintf!(L!("Event %ls\n"), evt.desc.typ.name()));
}
match &evt.desc.typ {
EventType::Signal { signal } => {
streams.out.append(&sprintf!(
L!("%ls %ls\n"),
sig2wcs(*signal),
evt.function_name
));
}
EventType::ProcessExit { .. } | EventType::JobExit { .. } => {}
EventType::CallerExit { .. } => {
streams
.out
.append(&sprintf!(L!("caller-exit %ls\n"), evt.function_name));
}
EventType::Variable { name: param } | EventType::Generic { param } => {
streams
.out
.append(&sprintf!(L!("%ls %ls\n"), param, evt.function_name));
}
EventType::Any => unreachable!(),
}
}
}
fn event_print_ffi(streams: Pin<&mut ffi::io_streams_t>, type_filter: &CxxWString) {
let mut streams = io_streams_t::new(streams);
print(&mut streams, &type_filter.from_ffi());
}
/// Fire a generic event with the specified name.
pub fn fire_generic(parser: &mut parser_t, name: WString, arguments: Vec<WString>) {
fire(
parser,
Event {
desc: EventDescription {
typ: EventType::Generic { param: name },
},
arguments,
},
)
}
fn event_fire_generic_ffi(
parser: Pin<&mut parser_t>,
name: &CxxWString,
arguments: &CxxVector<wcharz_t>,
) {
fire_generic(
parser.unpin(),
name.from_ffi(),
arguments.iter().map(WString::from).collect(),
);
}

View File

@ -1,39 +0,0 @@
use crate::wchar::{EXPAND_RESERVED_BASE, EXPAND_RESERVED_END};
/// Private use area characters used in expansions
#[repr(u32)]
pub enum ExpandChars {
/// Character representing a home directory.
HomeDirectory = EXPAND_RESERVED_BASE as u32,
/// Character representing process expansion for %self.
ProcessExpandSelf,
/// Character representing variable expansion.
VariableExpand,
/// Character representing variable expansion into a single element.
VariableExpandSingle,
/// Character representing the start of a bracket expansion.
BraceBegin,
/// Character representing the end of a bracket expansion.
BraceEnd,
/// Character representing separation between two bracket elements.
BraceSep,
/// Character that takes the place of any whitespace within non-quoted text in braces
BraceSpace,
/// Separate subtokens in a token with this character.
InternalSeparator,
/// Character representing an empty variable expansion. Only used transitively while expanding
/// variables.
VariableExpandEmpty,
}
const _: () = assert!(
EXPAND_RESERVED_END as u32 > ExpandChars::VariableExpandEmpty as u32,
"Characters used in expansions must stay within private use area"
);
impl From<ExpandChars> for char {
fn from(val: ExpandChars) -> Self {
// We know this is safe because we limit the the range of this enum
unsafe { char::from_u32_unchecked(val as _) }
}
}

View File

@ -1,599 +0,0 @@
use std::os::fd::{AsRawFd, RawFd};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
pub use self::fd_monitor_ffi::ItemWakeReason;
use self::fd_monitor_ffi::{new_fd_event_signaller, FdEventSignaller};
use crate::fd_readable_set::FdReadableSet;
use crate::fds::AutoCloseFd;
use crate::ffi::void_ptr;
use crate::flog::FLOG;
use crate::threads::assert_is_background_thread;
use crate::wutil::perror;
use cxx::SharedPtr;
#[cxx::bridge]
mod fd_monitor_ffi {
/// Reason for waking an item
#[repr(u8)]
#[cxx_name = "item_wake_reason_t"]
enum ItemWakeReason {
/// The fd became readable (or was HUP'd)
Readable,
/// The requested timeout was hit
Timeout,
/// The item was "poked" (woken up explicitly)
Poke,
}
unsafe extern "C++" {
include!("fds.h");
/// An event signaller implemented using a file descriptor, so it can plug into
/// [`select()`](libc::select).
///
/// This is like a binary semaphore. A call to [`post()`](FdEventSignaller::post) will
/// signal an event, making the fd readable. Multiple calls to `post()` may be coalesced.
/// On Linux this uses [`eventfd()`](libc::eventfd), on other systems this uses a pipe.
/// [`try_consume()`](FdEventSignaller::try_consume) may be used to consume the event.
/// Importantly this is async signal safe. Of course it is `CLO_EXEC` as well.
#[rust_name = "FdEventSignaller"]
type fd_event_signaller_t = crate::ffi::fd_event_signaller_t;
#[rust_name = "new_fd_event_signaller"]
fn ffi_new_fd_event_signaller_t() -> SharedPtr<FdEventSignaller>;
}
extern "Rust" {
#[cxx_name = "fd_monitor_item_id_t"]
type FdMonitorItemId;
}
extern "Rust" {
#[cxx_name = "fd_monitor_item_t"]
type FdMonitorItem;
#[cxx_name = "make_fd_monitor_item_t"]
fn new_fd_monitor_item_ffi(
fd: i32,
timeout_usecs: u64,
callback: *const u8,
param: *const u8,
) -> Box<FdMonitorItem>;
}
extern "Rust" {
#[cxx_name = "fd_monitor_t"]
type FdMonitor;
#[cxx_name = "make_fd_monitor_t"]
fn new_fd_monitor_ffi() -> Box<FdMonitor>;
#[cxx_name = "add_item"]
fn add_item_ffi(
&mut self,
fd: i32,
timeout_usecs: u64,
callback: *const u8,
param: *const u8,
) -> u64;
#[cxx_name = "poke_item"]
fn poke_item_ffi(&self, item_id: u64);
#[cxx_name = "add"]
pub fn add_ffi(&mut self, item: Box<FdMonitorItem>) -> u64;
}
}
// TODO: Remove once we're no longer using the FFI variant of FdEventSignaller
unsafe impl Sync for FdEventSignaller {}
unsafe impl Send for FdEventSignaller {}
/// Each item added to fd_monitor_t is assigned a unique ID, which is not recycled. Items may have
/// their callback triggered immediately by passing the ID. Zero is a sentinel.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct FdMonitorItemId(u64);
impl From<FdMonitorItemId> for u64 {
fn from(value: FdMonitorItemId) -> Self {
value.0
}
}
impl From<u64> for FdMonitorItemId {
fn from(value: u64) -> Self {
FdMonitorItemId(value)
}
}
type FfiCallback = extern "C" fn(*mut AutoCloseFd, u8, void_ptr);
type NativeCallback = Box<dyn Fn(&mut AutoCloseFd, ItemWakeReason) + Send + Sync>;
/// The callback type used by [`FdMonitorItem`]. It is passed a mutable reference to the
/// `FdMonitorItem`'s [`FdMonitorItem::fd`] and [the reason](ItemWakeupReason) for the wakeup. The
/// callback may close the fd, in which case the `FdMonitorItem` is removed from [`FdMonitor`]'s
/// set.
///
/// As capturing C++ closures can't be safely used via ffi interop and cxx bridge doesn't support
/// passing typed `fn(...)` pointers from C++ to rust, we have a separate variant of the type that
/// uses the C abi to invoke a callback. This will be removed when the dependent C++ code (currently
/// only `src/io.cpp`) is ported to rust
enum FdMonitorCallback {
None,
Native(NativeCallback),
Ffi(FfiCallback /* fn ptr */, void_ptr /* param */),
}
/// An item containing an fd and callback, which can be monitored to watch when it becomes readable
/// and invoke the callback.
pub struct FdMonitorItem {
/// The fd to monitor
fd: AutoCloseFd,
/// A callback to be invoked when the fd is readable, or when we are timed out. If we time out,
/// then timed_out will be true. If the fd is invalid on return from the function, then the item
/// is removed from the [`FdMonitor`] set.
callback: FdMonitorCallback,
/// The timeout associated with waiting on this item or `None` to wait indefinitely. A timeout
/// of `0` is not supported.
timeout: Option<Duration>,
/// The last time we were called or the time of initialization.
last_time: Option<Instant>,
/// The id for this item, assigned by [`FdMonitor`].
item_id: FdMonitorItemId,
}
/// Unlike C++, rust's `Vec` has `Vec::retain()` instead of `std::remove_if(...)` with the inverse
/// logic. It's hard to keep track of which bool means what across the different layers, so be more
/// explicit.
#[derive(PartialEq, Eq)]
enum ItemAction {
Remove,
Retain,
}
impl FdMonitorItem {
/// Returns the id for this `FdMonitorItem` that is registered with the [`FdMonitor`].
pub fn id(&self) -> FdMonitorItemId {
self.item_id
}
/// Return the duration until the timeout should trigger or `None`. A return of `0` means we are
/// at or past the timeout.
fn remaining_time(&self, now: &Instant) -> Option<Duration> {
let last_time = self.last_time.expect("Should always have a last_time!");
let timeout = self.timeout?;
assert!(now >= &last_time, "Steady clock went backwards or bug!");
let since = *now - last_time;
Some(if since >= timeout {
Duration::ZERO
} else {
timeout - since
})
}
/// Invoke this item's callback if its value (when its value is set in the fd or has timed out).
/// Returns `true` if the item should be retained or `false` if it should be removed from the
/// set.
fn service_item(&mut self, fds: &FdReadableSet, now: &Instant) -> ItemAction {
let mut result = ItemAction::Retain;
let readable = fds.test(self.fd.as_raw_fd());
let timed_out = !readable && self.remaining_time(now) == Some(Duration::ZERO);
if readable || timed_out {
self.last_time = Some(*now);
let reason = if readable {
ItemWakeReason::Readable
} else {
ItemWakeReason::Timeout
};
match &self.callback {
FdMonitorCallback::None => panic!("Callback not assigned!"),
FdMonitorCallback::Native(callback) => (callback)(&mut self.fd, reason),
FdMonitorCallback::Ffi(callback, param) => {
// Safety: identical objects are generated on both sides by cxx bridge as
// integers of the same size (minimum size to fit the enum).
let reason = unsafe { std::mem::transmute(reason) };
(callback)(&mut self.fd as *mut _, reason, *param)
}
}
if !self.fd.is_valid() {
result = ItemAction::Remove;
}
}
return result;
}
/// Invoke this item's callback with a poke, if its id is present in the sorted poke list.
fn maybe_poke_item(&mut self, pokelist: &[FdMonitorItemId]) -> ItemAction {
if self.item_id.0 == 0 || pokelist.binary_search(&self.item_id).is_err() {
// Not pokeable or not in the poke list.
return ItemAction::Retain;
}
match &self.callback {
FdMonitorCallback::None => panic!("Callback not assigned!"),
FdMonitorCallback::Native(callback) => (callback)(&mut self.fd, ItemWakeReason::Poke),
FdMonitorCallback::Ffi(callback, param) => {
// Safety: identical objects are generated on both sides by cxx bridge as
// integers of the same size (minimum size to fit the enum).
let reason = unsafe { std::mem::transmute(ItemWakeReason::Poke) };
(callback)(&mut self.fd as *mut _, reason, *param)
}
}
// Return `ItemAction::Remove` if the callback closed the fd
match self.fd.is_valid() {
true => ItemAction::Retain,
false => ItemAction::Remove,
}
}
pub fn new(
fd: AutoCloseFd,
timeout: Option<Duration>,
callback: Option<NativeCallback>,
) -> Self {
FdMonitorItem {
fd,
timeout,
callback: match callback {
Some(callback) => FdMonitorCallback::Native(callback),
None => FdMonitorCallback::None,
},
item_id: FdMonitorItemId(0),
last_time: None,
}
}
pub fn set_callback(&mut self, callback: NativeCallback) {
self.callback = FdMonitorCallback::Native(callback);
}
fn set_callback_ffi(&mut self, callback: *const u8, param: *const u8) {
// Safety: we are just marshalling our function pointers with identical definitions on both
// sides of the ffi bridge as void pointers to keep cxx bridge happy. Whether we invoke the
// raw function as a void pointer or as a typed fn that helps us keep track of what we're
// doing is unsafe in all cases, so might as well make the best of it.
let callback = unsafe { std::mem::transmute(callback) };
self.callback = FdMonitorCallback::Ffi(callback, param.into());
}
}
impl Default for FdMonitorItem {
fn default() -> Self {
Self {
callback: FdMonitorCallback::None,
fd: AutoCloseFd::empty(),
timeout: None,
last_time: None,
item_id: FdMonitorItemId(0),
}
}
}
// cxx bridge does not support "static member functions" in C++ or rust, so we need a top-level fn.
fn new_fd_monitor_ffi() -> Box<FdMonitor> {
Box::new(FdMonitor::new())
}
// cxx bridge does not support "static member functions" in C++ or rust, so we need a top-level fn.
fn new_fd_monitor_item_ffi(
fd: RawFd,
timeout_usecs: u64,
callback: *const u8,
param: *const u8,
) -> Box<FdMonitorItem> {
// Safety: we are just marshalling our function pointers with identical definitions on both
// sides of the ffi bridge as void pointers to keep cxx bridge happy. Whether we invoke the
// raw function as a void pointer or as a typed fn that helps us keep track of what we're
// doing is unsafe in all cases, so might as well make the best of it.
let callback = unsafe { std::mem::transmute(callback) };
let mut item = FdMonitorItem::default();
item.fd.reset(fd);
item.callback = FdMonitorCallback::Ffi(callback, param.into());
if timeout_usecs != FdReadableSet::kNoTimeout {
item.timeout = Some(Duration::from_micros(timeout_usecs));
}
return Box::new(item);
}
/// A thread-safe class which can monitor a set of fds, invoking a callback when any becomes
/// readable (or has been HUP'd) or when per-item-configurable timeouts are reached.
pub struct FdMonitor {
/// Our self-signaller. When this is written to, it means there are new items pending, new items
/// in the poke list, or terminate has been set.
change_signaller: SharedPtr<FdEventSignaller>,
/// The data shared between the background thread and the `FdMonitor` instance.
data: Arc<Mutex<SharedData>>,
/// The last ID assigned or `0` if none.
last_id: AtomicU64,
}
// We don't want to manually implement `Sync` for `FdMonitor` but we do want to make sure that it's
// always using interior mutability correctly and therefore automatically `Sync`.
const _: () = {
// It is sufficient to declare the generic function pointers; calling them too would require
// using `const fn` with Send/Sync constraints which wasn't stabilized until rustc 1.61.0
fn assert_sync<T: Sync>() {}
let _ = assert_sync::<FdMonitor>;
};
/// Data shared between the `FdMonitor` instance and its associated `BackgroundFdMonitor`.
struct SharedData {
/// Pending items. This is set by the main thread with the mutex locked, then the background
/// thread grabs them.
pending: Vec<FdMonitorItem>,
/// List of IDs for items that need to be poked (explicitly woken up).
pokelist: Vec<FdMonitorItemId>,
/// Whether the background thread is running.
running: bool,
/// Used to signal that the background thread should terminate.
terminate: bool,
}
/// The background half of the fd monitor, running on its own thread.
struct BackgroundFdMonitor {
/// The list of items to monitor. This is only accessed from the background thread.
/// This doesn't need to be in any particular order.
items: Vec<FdMonitorItem>,
/// Our self-signaller. When this is written to, it means there are new items pending, new items
/// in the poke list, or terminate has been set.
change_signaller: SharedPtr<FdEventSignaller>,
/// The data shared between the background thread and the `FdMonitor` instance.
data: Arc<Mutex<SharedData>>,
}
impl FdMonitor {
#[allow(clippy::boxed_local)]
pub fn add_ffi(&self, item: Box<FdMonitorItem>) -> u64 {
self.add(*item).0
}
/// Add an item to the monitor. Returns the [`FdMonitorItemId`] assigned to the item.
pub fn add(&self, mut item: FdMonitorItem) -> FdMonitorItemId {
assert!(item.fd.is_valid());
assert!(item.timeout != Some(Duration::ZERO), "Invalid timeout!");
assert!(
item.item_id == FdMonitorItemId(0),
"Item should not already have an id!"
);
let item_id = self.last_id.fetch_add(1, Ordering::Relaxed) + 1;
let item_id = FdMonitorItemId(item_id);
let start_thread = {
// Lock around a local region
let mut data = self.data.lock().expect("Mutex poisoned!");
// Assign an id and add the item to pending
item.item_id = item_id;
data.pending.push(item);
// Start the thread if it hasn't already been started
let already_started = data.running;
data.running = true;
!already_started
};
if start_thread {
FLOG!(fd_monitor, "Thread starting");
let background_monitor = BackgroundFdMonitor {
data: Arc::clone(&self.data),
change_signaller: SharedPtr::clone(&self.change_signaller),
items: Vec::new(),
};
crate::threads::spawn(move || {
background_monitor.run();
});
}
item_id
}
/// Avoid requiring a separate UniquePtr for each item C++ wants to add to the set by giving an
/// all-in-one entry point that can initialize the item on our end and insert it to the set.
fn add_item_ffi(
&mut self,
fd: RawFd,
timeout_usecs: u64,
callback: *const u8,
param: *const u8,
) -> u64 {
// Safety: we are just marshalling our function pointers with identical definitions on both
// sides of the ffi bridge as void pointers to keep cxx bridge happy. Whether we invoke the
// raw function as a void pointer or as a typed fn that helps us keep track of what we're
// doing is unsafe in all cases, so might as well make the best of it.
let callback = unsafe { std::mem::transmute(callback) };
let mut item = FdMonitorItem::default();
item.fd.reset(fd);
item.callback = FdMonitorCallback::Ffi(callback, param.into());
if timeout_usecs != FdReadableSet::kNoTimeout {
item.timeout = Some(Duration::from_micros(timeout_usecs));
}
self.add(item).0
}
/// Mark that the item with the given ID needs to be woken up explicitly.
pub fn poke_item(&self, item_id: FdMonitorItemId) {
assert!(item_id.0 > 0, "Invalid item id!");
let needs_notification = {
let mut data = self.data.lock().expect("Mutex poisoned!");
let needs_notification = data.pokelist.is_empty();
// Insert it, sorted. But not if it already exists.
if let Err(pos) = data.pokelist.binary_search(&item_id) {
data.pokelist.insert(pos, item_id);
};
needs_notification
};
if needs_notification {
self.change_signaller.post();
}
}
fn poke_item_ffi(&self, item_id: u64) {
self.poke_item(FdMonitorItemId(item_id))
}
pub fn new() -> Self {
Self {
data: Arc::new(Mutex::new(SharedData {
pending: Vec::new(),
pokelist: Vec::new(),
running: false,
terminate: false,
})),
change_signaller: new_fd_event_signaller(),
last_id: AtomicU64::new(0),
}
}
}
impl BackgroundFdMonitor {
/// Starts monitoring the fd set and listening for new fds to add to the set. Takes ownership
/// over its instance so that this method cannot be called again.
fn run(mut self) {
assert_is_background_thread();
let mut pokelist: Vec<FdMonitorItemId> = Vec::new();
let mut fds = FdReadableSet::new();
loop {
// Poke any items that need it
if !pokelist.is_empty() {
self.poke(&pokelist);
pokelist.clear();
}
fds.clear();
// Our change_signaller is special-cased
let change_signal_fd = self.change_signaller.read_fd().into();
fds.add(change_signal_fd);
let mut now = Instant::now();
// Use Duration::MAX to represent no timeout for comparison purposes.
let mut timeout = Duration::MAX;
for item in &mut self.items {
fds.add(item.fd.as_raw_fd());
if item.last_time.is_none() {
item.last_time = Some(now);
}
timeout = timeout.min(item.timeout.unwrap_or(Duration::MAX));
}
// If we have no items, then we wish to allow the thread to exit, but after a time, so
// we aren't spinning up and tearing down the thread repeatedly. Set a timeout of 256
// msec; if nothing becomes readable by then we will exit. We refer to this as the
// wait-lap.
let is_wait_lap = self.items.is_empty();
if is_wait_lap {
assert!(
timeout == Duration::MAX,
"Should not have a timeout on wait lap!"
);
timeout = Duration::from_millis(256);
}
// Don't leave Duration::MAX as an actual timeout value
let timeout = match timeout {
Duration::MAX => None,
timeout => Some(timeout),
};
// Call select()
let ret = fds.check_readable(
timeout
.map(|duration| duration.as_micros() as u64)
.unwrap_or(FdReadableSet::kNoTimeout),
);
if ret < 0 && errno::errno().0 != libc::EINTR {
// Surprising error
perror("select");
}
// Update the value of `now` after waiting on `fds.check_readable()`; it's used in the
// servicer closure.
now = Instant::now();
// A predicate which services each item in turn, returning true if it should be removed
let servicer = |item: &mut FdMonitorItem| {
let fd = item.fd.as_raw_fd();
if item.service_item(&fds, &now) == ItemAction::Remove {
FLOG!(fd_monitor, "Removing fd", fd);
return ItemAction::Remove;
}
return ItemAction::Retain;
};
// Service all items that are either readable or have timed out, and remove any which
// say to do so.
self.items
.retain_mut(|item| servicer(item) == ItemAction::Retain);
// Handle any changes if the change signaller was set. Alternatively, this may be the
// wait lap, in which case we might want to commit to exiting.
let change_signalled = fds.test(change_signal_fd);
if change_signalled || is_wait_lap {
// Clear the change signaller before processing incoming changes
self.change_signaller.try_consume();
let mut data = self.data.lock().expect("Mutex poisoned!");
// Move from `pending` to the end of `items`
self.items.extend(&mut data.pending.drain(..));
// Grab any poke list
assert!(
pokelist.is_empty(),
"poke list should be empty or else we're dropping pokes!"
);
std::mem::swap(&mut pokelist, &mut data.pokelist);
if data.terminate
|| (is_wait_lap
&& self.items.is_empty()
&& pokelist.is_empty()
&& !change_signalled)
{
// Maybe terminate is set. Alternatively, maybe we had no items, waited a bit,
// and still have no items. It's important to do this while holding the lock,
// otherwise we race with new items being added.
assert!(
data.running,
"Thread should be running because we're that thread"
);
FLOG!(fd_monitor, "Thread exiting");
data.running = false;
break;
}
}
}
}
/// Poke items in the poke list, removing any items that close their fd in their callback. The
/// poke list is consumed after this. This is only called from the background thread.
fn poke(&mut self, pokelist: &[FdMonitorItemId]) {
self.items.retain_mut(|item| {
let action = item.maybe_poke_item(pokelist);
if action == ItemAction::Remove {
FLOG!(fd_monitor, "Removing fd", item.fd.as_raw_fd());
}
return action == ItemAction::Retain;
});
}
}
/// In ordinary usage, we never invoke the destructor. This is used in the tests to not leave stale
/// fds arounds; this is why it's very hacky!
impl Drop for FdMonitor {
fn drop(&mut self) {
// Safety: this is a port of the C++ code and we are running in the destructor. The C++ code
// had no way to bubble back any errors encountered here, and the pthread mutex the C++ code
// uses does not have a concept of mutex poisoning.
self.data.lock().expect("Mutex poisoned!").terminate = true;
self.change_signaller.post();
// Safety: see note above.
while self.data.lock().expect("Mutex poisoned!").running {
std::thread::sleep(Duration::from_millis(5));
}
}
}

View File

@ -1,250 +0,0 @@
use libc::c_int;
use std::os::unix::io::RawFd;
pub use fd_readable_set_t as FdReadableSet;
#[cxx::bridge]
mod fd_readable_set_ffi {
extern "Rust" {
type fd_readable_set_t;
fn new_fd_readable_set() -> Box<fd_readable_set_t>;
fn clear(&mut self);
fn add(&mut self, fd: i32);
fn test(&self, fd: i32) -> bool;
fn check_readable(&mut self, timeout_usec: u64) -> i32;
fn is_fd_readable(fd: i32, timeout_usec: u64) -> bool;
fn poll_fd_readable(fd: i32) -> bool;
}
}
/// Create a new fd_readable_set_t.
pub fn new_fd_readable_set() -> Box<fd_readable_set_t> {
Box::new(fd_readable_set_t::new())
}
/// Returns `true` if the fd is or becomes readable within the given timeout.
/// This returns `false` if the waiting is interrupted by a signal.
pub fn is_fd_readable(fd: i32, timeout_usec: u64) -> bool {
fd_readable_set_t::is_fd_readable(fd, timeout_usec)
}
/// Returns whether an fd is readable.
pub fn poll_fd_readable(fd: i32) -> bool {
fd_readable_set_t::poll_fd_readable(fd)
}
/// A modest wrapper around select() or poll().
/// This allows accumulating a set of fds and then seeing if they are readable.
/// This only handles readability.
/// Apple's `man poll`: "The poll() system call currently does not support devices."
#[cfg(target_os = "macos")]
pub struct fd_readable_set_t {
// The underlying fdset and nfds value to pass to select().
fdset_: libc::fd_set,
nfds_: c_int,
}
const kUsecPerMsec: u64 = 1000;
const kUsecPerSec: u64 = 1000 * kUsecPerMsec;
#[cfg(target_os = "macos")]
impl fd_readable_set_t {
/// Construct an empty set.
pub fn new() -> fd_readable_set_t {
fd_readable_set_t {
fdset_: unsafe { std::mem::zeroed() },
nfds_: 0,
}
}
/// Reset back to an empty set.
pub fn clear(&mut self) {
self.nfds_ = 0;
unsafe {
libc::FD_ZERO(&mut self.fdset_);
}
}
/// Add an fd to the set. The fd is ignored if negative (for convenience).
pub fn add(&mut self, fd: RawFd) {
if fd >= (libc::FD_SETSIZE as RawFd) {
//FLOGF(error, "fd %d too large for select()", fd);
return;
}
if fd >= 0 {
unsafe { libc::FD_SET(fd, &mut self.fdset_) };
self.nfds_ = std::cmp::max(self.nfds_, fd + 1);
}
}
/// Returns `true` if the given `fd` is marked as set, in our set. Returns `false` if `fd` is
/// negative.
pub fn test(&self, fd: RawFd) -> bool {
fd >= 0 && unsafe { libc::FD_ISSET(fd, &self.fdset_) }
}
/// Call `select()` or `poll()`, according to FISH_READABLE_SET_USE_POLL. Note this
/// destructively modifies the set. Returns the result of `select()` or `poll()`.
pub fn check_readable(&mut self, timeout_usec: u64) -> c_int {
let null = std::ptr::null_mut();
if timeout_usec == Self::kNoTimeout {
unsafe {
return libc::select(
self.nfds_,
&mut self.fdset_,
null,
null,
std::ptr::null_mut(),
);
}
} else {
let mut tvs = libc::timeval {
tv_sec: (timeout_usec / kUsecPerSec) as libc::time_t,
tv_usec: (timeout_usec % kUsecPerSec) as libc::suseconds_t,
};
unsafe {
return libc::select(self.nfds_, &mut self.fdset_, null, null, &mut tvs);
}
}
}
/// Check if a single fd is readable, with a given timeout.
/// Returns `true` if readable, `false` otherwise.
pub fn is_fd_readable(fd: RawFd, timeout_usec: u64) -> bool {
if fd < 0 {
return false;
}
let mut s = Self::new();
s.add(fd);
let res = s.check_readable(timeout_usec);
return res > 0 && s.test(fd);
}
/// Check if a single fd is readable, without blocking.
/// Returns `true` if readable, `false` if not.
pub fn poll_fd_readable(fd: RawFd) -> bool {
return Self::is_fd_readable(fd, 0);
}
/// A special timeout value which may be passed to indicate no timeout.
pub const kNoTimeout: u64 = u64::MAX;
}
#[cfg(not(target_os = "macos"))]
pub struct fd_readable_set_t {
pollfds_: Vec<libc::pollfd>,
}
#[cfg(not(target_os = "macos"))]
impl fd_readable_set_t {
/// Construct an empty set.
pub fn new() -> fd_readable_set_t {
fd_readable_set_t {
pollfds_: Vec::new(),
}
}
/// Reset back to an empty set.
pub fn clear(&mut self) {
self.pollfds_.clear();
}
#[inline]
fn pollfd_get_fd(pollfd: &libc::pollfd) -> RawFd {
pollfd.fd
}
/// Add an fd to the set. The fd is ignored if negative (for convenience). The fd is also
/// ignored if it's already in the set.
pub fn add(&mut self, fd: RawFd) {
if fd < 0 {
return;
}
let pos = match self.pollfds_.binary_search_by_key(&fd, Self::pollfd_get_fd) {
Ok(_) => return,
Err(pos) => pos,
};
self.pollfds_.insert(
pos,
libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
},
);
}
/// Returns `true` if the given `fd` has input available to read or has been HUP'd.
/// Returns `false` if `fd` is negative or was not found in the set.
pub fn test(&self, fd: RawFd) -> bool {
// If a pipe is widowed with no data, Linux sets POLLHUP but not POLLIN, so test for both.
if let Ok(pos) = self.pollfds_.binary_search_by_key(&fd, Self::pollfd_get_fd) {
let pollfd = &self.pollfds_[pos];
debug_assert_eq!(pollfd.fd, fd);
return pollfd.revents & (libc::POLLIN | libc::POLLHUP) != 0;
}
return false;
}
/// Convert from usecs to poll-friendly msecs.
fn usec_to_poll_msec(timeout_usec: u64) -> c_int {
let mut timeout_msec: u64 = timeout_usec / kUsecPerMsec;
// Round to nearest, down for halfway.
if (timeout_usec % kUsecPerMsec) > kUsecPerMsec / 2 {
timeout_msec += 1;
}
if timeout_usec == fd_readable_set_t::kNoTimeout || timeout_msec > c_int::MAX as u64 {
// Negative values mean wait forever in poll-speak.
return -1;
}
return timeout_msec as c_int;
}
fn do_poll(fds: &mut [libc::pollfd], timeout_usec: u64) -> c_int {
let count = fds.len();
assert!(count <= libc::nfds_t::MAX as usize, "count too big");
return unsafe {
libc::poll(
fds.as_mut_ptr(),
count as libc::nfds_t,
Self::usec_to_poll_msec(timeout_usec),
)
};
}
/// Call select() or poll(), according to FISH_READABLE_SET_USE_POLL. Note this destructively
/// modifies the set. \return the result of select() or poll().
///
/// TODO: Change to [`Duration`](std::time::Duration) once FFI usage is done.
pub fn check_readable(&mut self, timeout_usec: u64) -> c_int {
if self.pollfds_.is_empty() {
return 0;
}
return Self::do_poll(&mut self.pollfds_, timeout_usec);
}
/// Check if a single fd is readable, with a given timeout.
/// \return true if `fd` is our set and is readable, `false` otherwise.
pub fn is_fd_readable(fd: RawFd, timeout_usec: u64) -> bool {
if fd < 0 {
return false;
}
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let ret = Self::do_poll(std::slice::from_mut(&mut pfd), timeout_usec);
return ret > 0 && (pfd.revents & libc::POLLIN) != 0;
}
/// Check if a single fd is readable, without blocking.
/// \return true if readable, false if not.
pub fn poll_fd_readable(fd: RawFd) -> bool {
return Self::is_fd_readable(fd, 0);
}
/// A special timeout value which may be passed to indicate no timeout.
pub const kNoTimeout: u64 = u64::MAX;
}

View File

@ -1,150 +0,0 @@
use crate::ffi;
use nix::unistd;
use std::io::{Read, Write};
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
/// A helper type for managing and automatically closing a file descriptor
///
/// This was implemented in rust as a port of the existing C++ code but it didn't take its place
/// (yet) and there's still the original cpp implementation in `src/fds.h`, so its name is
/// disambiguated because some code uses a mix of both for interop purposes.
pub struct AutoCloseFd {
fd_: RawFd,
}
impl Read for AutoCloseFd {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
unsafe {
match libc::read(self.as_raw_fd(), buf.as_mut_ptr() as *mut _, buf.len()) {
-1 => Err(std::io::Error::from_raw_os_error(errno::errno().0)),
bytes => Ok(bytes as usize),
}
}
}
}
impl Write for AutoCloseFd {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
unsafe {
match libc::write(self.as_raw_fd(), buf.as_ptr() as *const _, buf.len()) {
-1 => Err(std::io::Error::from_raw_os_error(errno::errno().0)),
bytes => Ok(bytes as usize),
}
}
}
fn flush(&mut self) -> std::io::Result<()> {
// We don't buffer anything so this is a no-op.
Ok(())
}
}
#[cxx::bridge]
mod autoclose_fd_t {
extern "Rust" {
#[cxx_name = "autoclose_fd_t2"]
type AutoCloseFd;
#[cxx_name = "valid"]
fn is_valid(&self) -> bool;
fn close(&mut self);
fn fd(&self) -> i32;
}
}
impl AutoCloseFd {
// Closes the fd if not already closed.
pub fn close(&mut self) {
if self.fd_ != -1 {
_ = unistd::close(self.fd_);
self.fd_ = -1;
}
}
// Returns the fd.
pub fn fd(&self) -> RawFd {
self.fd_
}
// Returns the fd, transferring ownership to the caller.
pub fn acquire(&mut self) -> RawFd {
let temp = self.fd_;
self.fd_ = -1;
temp
}
// Resets to a new fd, taking ownership.
pub fn reset(&mut self, fd: RawFd) {
if fd == self.fd_ {
return;
}
self.close();
self.fd_ = fd;
}
// Returns if this has a valid fd.
pub fn is_valid(&self) -> bool {
self.fd_ >= 0
}
// Create a new AutoCloseFd instance taking ownership of the passed fd
pub fn new(fd: RawFd) -> Self {
AutoCloseFd { fd_: fd }
}
// Create a new AutoCloseFd without an open fd
pub fn empty() -> Self {
AutoCloseFd { fd_: -1 }
}
}
impl FromRawFd for AutoCloseFd {
unsafe fn from_raw_fd(fd: RawFd) -> Self {
AutoCloseFd { fd_: fd }
}
}
impl AsRawFd for AutoCloseFd {
fn as_raw_fd(&self) -> RawFd {
self.fd()
}
}
impl Default for AutoCloseFd {
fn default() -> AutoCloseFd {
AutoCloseFd { fd_: -1 }
}
}
impl Drop for AutoCloseFd {
fn drop(&mut self) {
self.close()
}
}
/// Helper type returned from make_autoclose_pipes.
#[derive(Default)]
pub struct autoclose_pipes_t {
/// Read end of the pipe.
pub read: AutoCloseFd,
/// Write end of the pipe.
pub write: AutoCloseFd,
}
/// Construct a pair of connected pipes, set to close-on-exec.
/// \return None on fd exhaustion.
pub fn make_autoclose_pipes() -> Option<autoclose_pipes_t> {
let pipes = ffi::make_pipes_ffi();
let readp = AutoCloseFd::new(pipes.read);
let writep = AutoCloseFd::new(pipes.write);
if !readp.is_valid() || !writep.is_valid() {
None
} else {
Some(autoclose_pipes_t {
read: readp,
write: writep,
})
}
}

View File

@ -1,237 +0,0 @@
use crate::wchar;
use crate::wchar_ffi::WCharToFFI;
#[rustfmt::skip]
use ::std::fmt::{self, Debug, Formatter};
#[rustfmt::skip]
use ::std::pin::Pin;
#[rustfmt::skip]
use ::std::slice;
use crate::wchar::wstr;
use autocxx::prelude::*;
use cxx::SharedPtr;
use libc::pid_t;
// autocxx has been hacked up to know about this.
pub type wchar_t = u32;
include_cpp! {
#include "builtin.h"
#include "common.h"
#include "env.h"
#include "event.h"
#include "fallback.h"
#include "fds.h"
#include "flog.h"
#include "io.h"
#include "parse_constants.h"
#include "parser.h"
#include "parse_util.h"
#include "proc.h"
#include "re.h"
#include "tokenizer.h"
#include "wildcard.h"
#include "wutil.h"
#include "termsize.h"
safety!(unsafe_ffi)
generate_pod!("wcharz_t")
generate!("make_fd_nonblocking")
generate!("wperror")
generate_pod!("pipes_ffi_t")
generate!("env_stack_t")
generate!("make_pipes_ffi")
generate!("valid_var_name_char")
generate!("get_flog_file_fd")
generate!("parse_util_unescape_wildcards")
generate!("fish_wcwidth")
generate!("fish_wcswidth")
generate!("wildcard_match")
generate!("wgettext_ptr")
generate!("block_t")
generate!("parser_t")
generate!("job_t")
generate!("process_t")
generate!("library_data_t")
generate_pod!("library_data_pod_t")
generate!("proc_wait_any")
generate!("output_stream_t")
generate!("io_streams_t")
generate_pod!("RustFFIJobList")
generate_pod!("RustFFIProcList")
generate_pod!("RustBuiltin")
generate!("builtin_missing_argument")
generate!("builtin_unknown_option")
generate!("builtin_print_help")
generate!("builtin_print_error_trailer")
generate!("wait_handle_t")
generate!("wait_handle_store_t")
generate!("escape_string")
generate!("sig2wcs")
generate!("wcs2sig")
generate!("signal_get_desc")
generate!("fd_event_signaller_t")
generate_pod!("re::flags_t")
generate_pod!("re::re_error_t")
generate!("re::regex_t")
generate!("re::regex_result_ffi")
generate!("re::try_compile_ffi")
generate!("wcs2string")
generate!("str2wcstring")
generate!("signal_handle")
generate!("signal_check_cancel")
generate!("block_t")
generate!("block_type_t")
generate!("statuses_t")
generate!("io_chain_t")
generate!("termsize_container_t")
generate!("env_var_t")
}
impl parser_t {
pub fn get_block_at_index(&self, i: usize) -> Option<&block_t> {
let b = self.block_at_index(i);
unsafe { b.as_ref() }
}
pub fn get_jobs(&self) -> &[SharedPtr<job_t>] {
let ffi_jobs = self.ffi_jobs();
unsafe { slice::from_raw_parts(ffi_jobs.jobs, ffi_jobs.count) }
}
pub fn libdata_pod(&mut self) -> &mut library_data_pod_t {
let libdata = self.pin().ffi_libdata_pod();
unsafe { &mut *libdata }
}
pub fn remove_var(&mut self, var: &wstr, flags: c_int) -> c_int {
self.pin().remove_var_ffi(&var.to_ffi(), flags)
}
pub fn job_get_from_pid(&self, pid: pid_t) -> Option<&job_t> {
let job = self.ffi_job_get_from_pid(pid.into());
unsafe { job.as_ref() }
}
}
pub fn try_compile(anchored: &wstr, flags: &re::flags_t) -> Pin<Box<re::regex_result_ffi>> {
re::try_compile_ffi(&anchored.to_ffi(), flags).within_box()
}
impl job_t {
#[allow(clippy::mut_from_ref)]
pub fn get_procs(&self) -> &mut [UniquePtr<process_t>] {
let ffi_procs = self.ffi_processes();
unsafe { slice::from_raw_parts_mut(ffi_procs.procs, ffi_procs.count) }
}
}
/// Allow wcharz_t to be "into" wstr.
impl From<wcharz_t> for &wchar::wstr {
fn from(w: wcharz_t) -> Self {
let len = w.length();
let v = unsafe { slice::from_raw_parts(w.str_ as *const u32, len) };
wchar::wstr::from_slice(v).expect("Invalid UTF-32")
}
}
/// Allow wcharz_t to be "into" WString.
impl From<wcharz_t> for wchar::WString {
fn from(w: wcharz_t) -> Self {
let len = w.length();
let v = unsafe { slice::from_raw_parts(w.str_ as *const u32, len).to_vec() };
Self::from_vec(v).expect("Invalid UTF-32")
}
}
impl Debug for re::regex_t {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("regex_t")
}
}
/// A bogus trait for turning &mut Foo into Pin<&mut Foo>.
/// autocxx enforces that non-const methods must be called through Pin,
/// but this means we can't pass around mutable references to types like parser_t.
/// We also don't want to assert that parser_t is Unpin.
/// So we just allow constructing a pin from a mutable reference; none of the C++ code.
/// It's worth considering disabling this in cxx; for now we use this trait.
/// Eventually parser_t and io_streams_t will not require Pin so we just unsafe-it away.
pub trait Repin {
fn pin(&mut self) -> Pin<&mut Self> {
unsafe { Pin::new_unchecked(self) }
}
fn unpin(self: Pin<&mut Self>) -> &mut Self {
unsafe { self.get_unchecked_mut() }
}
}
// Implement Repin for our types.
impl Repin for block_t {}
impl Repin for env_stack_t {}
impl Repin for io_streams_t {}
impl Repin for job_t {}
impl Repin for output_stream_t {}
impl Repin for parser_t {}
impl Repin for process_t {}
impl Repin for re::regex_result_ffi {}
unsafe impl Send for re::regex_t {}
pub use autocxx::c_int;
pub use ffi::*;
pub use libc::c_char;
/// A version of [`* const core::ffi::c_void`] (or [`* const libc::c_void`], if you prefer) that
/// implements `Copy` and `Clone`, because those two don't. Used to represent a `void *` ptr for ffi
/// purposes.
#[repr(transparent)]
#[derive(Copy, Clone)]
pub struct void_ptr(pub *const core::ffi::c_void);
impl core::fmt::Debug for void_ptr {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:p}", &self.0)
}
}
unsafe impl Send for void_ptr {}
unsafe impl Sync for void_ptr {}
impl core::convert::From<*const core::ffi::c_void> for void_ptr {
fn from(value: *const core::ffi::c_void) -> Self {
Self(value as *const _)
}
}
impl core::convert::From<*const u8> for void_ptr {
fn from(value: *const u8) -> Self {
Self(value as *const _)
}
}
impl core::convert::From<*const autocxx::c_void> for void_ptr {
fn from(value: *const autocxx::c_void) -> Self {
Self(value as *const _)
}
}

View File

@ -1,28 +0,0 @@
/// Bridged functions concerned with initialization.
use crate::ffi::wcharz_t;
#[cxx::bridge]
mod ffi2 {
extern "C++" {
include!("wutil.h");
type wcharz_t = super::wcharz_t;
}
extern "Rust" {
fn rust_init();
fn rust_activate_flog_categories_by_pattern(wc_ptr: wcharz_t);
}
}
/// Entry point for Rust-specific initialization.
fn rust_init() {
crate::topic_monitor::topic_monitor_init();
crate::future_feature_flags::future_feature_flags_init();
crate::threads::init();
}
/// FFI bridge for activate_flog_categories_by_pattern().
fn rust_activate_flog_categories_by_pattern(wc_ptr: wcharz_t) {
crate::flog::activate_flog_categories_by_pattern(wc_ptr.into());
}

View File

@ -1,63 +0,0 @@
//! Support for tests which need to cross the FFI.
//!
//! Because the C++ is not compiled by `cargo test` and there is no natural way to
//! do it, use the following facilities for tests which need to use C++ types.
//! This uses the inventory crate to build a custom-test harness
//! as described at <https://www.infinyon.com/blog/2021/04/rust-custom-test-harness/>
//! See smoke.rs add_test for an example of how to use this.
#[cfg(all(feature = "fish-ffi-tests", not(test)))]
mod ffi_tests_impl {
/// A test which needs to cross the FFI.
#[derive(Debug)]
pub struct FFITest {
pub name: &'static str,
pub func: fn(),
}
/// Add a new test.
/// Example usage:
/// ```
/// add_test!("test_name", || {
/// assert!(1 + 2 == 3);
/// });
/// ```
macro_rules! add_test {
($name:literal, $func:expr) => {
inventory::submit!(crate::ffi_tests::FFITest {
name: $name,
func: $func,
});
};
}
pub(crate) use add_test;
inventory::collect!(crate::ffi_tests::FFITest);
/// Runs all ffi tests.
pub fn run_ffi_tests() {
for test in inventory::iter::<crate::ffi_tests::FFITest> {
println!("Running ffi test {}", test.name);
(test.func)();
}
}
}
#[cfg(not(all(feature = "fish-ffi-tests", not(test))))]
mod ffi_tests_impl {
macro_rules! add_test {
($name:literal, $func:expr) => {};
}
pub(crate) use add_test;
pub fn run_ffi_tests() {}
}
pub(crate) use ffi_tests_impl::*;
#[allow(clippy::module_inception)]
#[cxx::bridge(namespace = rust)]
mod ffi_tests {
extern "Rust" {
fn run_ffi_tests();
}
}

View File

@ -1,224 +0,0 @@
use crate::ffi::{get_flog_file_fd, parse_util_unescape_wildcards, wildcard_match};
use crate::wchar::{widestrs, wstr, WString};
use crate::wchar_ffi::WCharToFFI;
use std::io::Write;
use std::os::unix::io::{FromRawFd, IntoRawFd, RawFd};
use std::sync::atomic::Ordering;
#[rustfmt::skip::macros(category)]
#[widestrs]
pub mod categories {
use super::wstr;
use std::sync::atomic::AtomicBool;
pub struct category_t {
pub name: &'static wstr,
pub description: &'static wstr,
pub enabled: AtomicBool,
}
/// Macro to declare a static variable identified by $var,
/// with the given name and description, and optionally enabled by default.
macro_rules! declare_category {
(
($var:ident, $name:expr, $description:expr, $enabled:expr)
) => {
pub static $var: category_t = category_t {
name: $name,
description: $description,
enabled: AtomicBool::new($enabled),
};
};
(
($var:ident, $name:expr, $description:expr)
) => {
declare_category!(($var, $name, $description, false));
};
}
/// Macro to extract the variable name for a category.
macro_rules! category_name {
(($var:ident, $name:expr, $description:expr, $enabled:expr)) => {
$var
};
(($var:ident, $name:expr, $description:expr)) => {
$var
};
}
macro_rules! categories {
(
// A repetition of categories, separated by semicolons.
$($cats:tt);*
// Allow trailing semicolon.
$(;)?
) => {
// Declare each category.
$(
declare_category!($cats);
)*
// Define a function which gives you a Vector of all categories.
pub fn all_categories() -> Vec<&'static category_t> {
vec![
$(
& category_name!($cats),
)*
]
}
};
}
categories!(
(error, "error"L, "Serious unexpected errors (on by default)"L, true);
(debug, "debug"L, "Debugging aid (on by default)"L, true);
(warning, "warning"L, "Warnings (on by default)"L, true);
(warning_path, "warning-path"L, "Warnings about unusable paths for config/history (on by default)"L, true);
(config, "config"L, "Finding and reading configuration"L);
(event, "event"L, "Firing events"L);
(exec, "exec"L, "Errors reported by exec (on by default)"L, true);
(exec_job_status, "exec-job-status"L, "Jobs changing status"L);
(exec_job_exec, "exec-job-exec"L, "Jobs being executed"L);
(exec_fork, "exec-fork"L, "Calls to fork()"L);
(output_invalid, "output-invalid"L, "Trying to print invalid output"L);
(ast_construction, "ast-construction"L, "Parsing fish AST"L);
(proc_job_run, "proc-job-run"L, "Jobs getting started or continued"L);
(proc_termowner, "proc-termowner"L, "Terminal ownership events"L);
(proc_internal_proc, "proc-internal-proc"L, "Internal (non-forked) process events"L);
(proc_reap_internal, "proc-reap-internal"L, "Reaping internal (non-forked) processes"L);
(proc_reap_external, "proc-reap-external"L, "Reaping external (forked) processes"L);
(proc_pgroup, "proc-pgroup"L, "Process groups"L);
(env_locale, "env-locale"L, "Changes to locale variables"L);
(env_export, "env-export"L, "Changes to exported variables"L);
(env_dispatch, "env-dispatch"L, "Reacting to variables"L);
(uvar_file, "uvar-file"L, "Writing/reading the universal variable store"L);
(uvar_notifier, "uvar-notifier"L, "Notifications about universal variable changes"L);
(topic_monitor, "topic-monitor"L, "Internal details of the topic monitor"L);
(char_encoding, "char-encoding"L, "Character encoding issues"L);
(history, "history"L, "Command history events"L);
(history_file, "history-file"L, "Reading/Writing the history file"L);
(profile_history, "profile-history"L, "History performance measurements"L);
(iothread, "iothread"L, "Background IO thread events"L);
(fd_monitor, "fd-monitor"L, "FD monitor events"L);
(term_support, "term-support"L, "Terminal feature detection"L);
(reader, "reader"L, "The interactive reader/input system"L);
(reader_render, "reader-render"L, "Rendering the command line"L);
(complete, "complete"L, "The completion system"L);
(path, "path"L, "Searching/using paths"L);
(screen, "screen"L, "Screen repaints"L);
);
}
/// FLOG formats values. By default we would like to use Display, and fall back to Debug.
/// However that would require specialization. So instead we make two "separate" traits, bring them both in scope,
/// and let Rust figure it out.
/// Clients can opt a Debug type into Floggable by implementing FloggableDebug:
/// impl FloggableDebug for MyType {}
pub trait FloggableDisplay {
/// Return a string representation of this thing.
fn to_flog_str(&self) -> String;
}
impl<T: std::fmt::Display> FloggableDisplay for T {
fn to_flog_str(&self) -> String {
format!("{}", self)
}
}
pub trait FloggableDebug: std::fmt::Debug {
fn to_flog_str(&self) -> String {
format!("{:?}", self)
}
}
/// Write to our FLOG file.
pub fn flog_impl(s: &str) {
let fd = get_flog_file_fd().0 as RawFd;
if fd < 0 {
return;
}
let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
let _ = file.write(s.as_bytes());
// Ensure the file is not closed.
file.into_raw_fd();
}
macro_rules! FLOG {
($category:ident, $($elem:expr),+) => {
if crate::flog::categories::$category.enabled.load(std::sync::atomic::Ordering::Relaxed) {
#[allow(unused_imports)]
use crate::flog::{FloggableDisplay, FloggableDebug};
let mut vs = Vec::new();
$(
{
vs.push($elem.to_flog_str())
}
)+
// We don't use locking here so we have to append our own newline to avoid multiple writes.
let mut v = vs.join(" ");
v.push('\n');
crate::flog::flog_impl(&v);
}
};
}
pub(crate) use FLOG;
/// For each category, if its name matches the wildcard, set its enabled to the given sense.
fn apply_one_wildcard(wc_esc: &wstr, sense: bool) {
let wc = parse_util_unescape_wildcards(&wc_esc.to_ffi());
let mut match_found = false;
for cat in categories::all_categories() {
if wildcard_match(&cat.name.to_ffi(), &wc, false) {
cat.enabled.store(sense, Ordering::Relaxed);
match_found = true;
}
}
if !match_found {
eprintln!("Failed to match debug category: {wc_esc}");
}
}
/// Set the active flog categories according to the given wildcard \p wc.
pub fn activate_flog_categories_by_pattern(wc_ptr: &wstr) {
let mut wc: WString = wc_ptr.into();
// Normalize underscores to dashes, allowing the user to be sloppy.
for c in wc.as_char_slice_mut() {
if *c == '_' {
*c = '-';
}
}
for s in wc.as_char_slice().split(|c| *c == ',') {
if s.starts_with(&['-']) {
apply_one_wildcard(wstr::from_char_slice(&s[1..]), false);
} else {
apply_one_wildcard(wstr::from_char_slice(s), true);
}
}
}

View File

@ -1,254 +0,0 @@
//! Flags to enable upcoming features
use crate::ffi::wcharz_t;
use crate::wchar::wstr;
use crate::wchar_ffi::WCharToFFI;
use std::array;
use std::cell::UnsafeCell;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use widestring_suffix::widestrs;
#[cxx::bridge]
mod future_feature_flags_ffi {
extern "C++" {
include!("wutil.h");
type wcharz_t = super::wcharz_t;
}
/// The list of flags.
#[repr(u8)]
enum FeatureFlag {
/// Whether ^ is supported for stderr redirection.
stderr_nocaret,
/// Whether ? is supported as a glob.
qmark_noglob,
/// Whether string replace -r double-unescapes the replacement.
string_replace_backslash,
/// Whether "&" is not-special if followed by a word character.
ampersand_nobg_in_token,
}
/// Metadata about feature flags.
struct feature_metadata_t {
flag: FeatureFlag,
name: UniquePtr<CxxWString>,
groups: UniquePtr<CxxWString>,
description: UniquePtr<CxxWString>,
default_value: bool,
read_only: bool,
}
extern "Rust" {
type Features;
fn test(self: &Features, flag: FeatureFlag) -> bool;
fn set(self: &mut Features, flag: FeatureFlag, value: bool);
fn set_from_string(self: &mut Features, str: wcharz_t);
fn fish_features() -> *const Features;
fn feature_test(flag: FeatureFlag) -> bool;
fn mutable_fish_features() -> *mut Features;
fn feature_metadata() -> [feature_metadata_t; 4];
}
}
pub use future_feature_flags_ffi::{feature_metadata_t, FeatureFlag};
pub struct Features {
// Values for the flags.
// These are atomic to "fix" a race reported by tsan where tests of feature flags and other
// tests which use them conceptually race.
values: [AtomicBool; metadata.len()],
}
/// Metadata about feature flags.
struct FeatureMetadata {
/// The flag itself.
flag: FeatureFlag,
/// User-presentable short name of the feature flag.
name: &'static wstr,
/// Comma-separated list of feature groups.
groups: &'static wstr,
/// User-presentable description of the feature flag.
description: &'static wstr,
/// Default flag value.
default_value: bool,
/// Whether the value can still be changed or not.
read_only: bool,
}
impl From<&FeatureMetadata> for feature_metadata_t {
fn from(md: &FeatureMetadata) -> feature_metadata_t {
feature_metadata_t {
flag: md.flag,
name: md.name.to_ffi(),
groups: md.groups.to_ffi(),
description: md.description.to_ffi(),
default_value: md.default_value,
read_only: md.read_only,
}
}
}
/// The metadata, indexed by flag.
#[widestrs]
const metadata: [FeatureMetadata; 4] = [
FeatureMetadata {
flag: FeatureFlag::stderr_nocaret,
name: "stderr-nocaret"L,
groups: "3.0"L,
description: "^ no longer redirects stderr (historical, can no longer be changed)"L,
default_value: true,
read_only: true,
},
FeatureMetadata {
flag: FeatureFlag::qmark_noglob,
name: "qmark-noglob"L,
groups: "3.0"L,
description: "? no longer globs"L,
default_value: false,
read_only: false,
},
FeatureMetadata {
flag: FeatureFlag::string_replace_backslash,
name: "regex-easyesc"L,
groups: "3.1"L,
description: "string replace -r needs fewer \\'s"L,
default_value: true,
read_only: false,
},
FeatureMetadata {
flag: FeatureFlag::ampersand_nobg_in_token,
name: "ampersand-nobg-in-token"L,
groups: "3.4"L,
description: "& only backgrounds if followed by a separator"L,
default_value: true,
read_only: false,
},
];
/// The singleton shared feature set.
static mut global_features: *const UnsafeCell<Features> = std::ptr::null();
pub fn future_feature_flags_init() {
unsafe {
// Leak it for now.
global_features = Box::into_raw(Box::new(UnsafeCell::new(Features::new())));
}
}
impl Features {
fn new() -> Self {
Features {
values: array::from_fn(|i| AtomicBool::new(metadata[i].default_value)),
}
}
/// Return whether a flag is set.
pub fn test(&self, flag: FeatureFlag) -> bool {
self.values[flag.repr as usize].load(Ordering::SeqCst)
}
/// Set a flag.
pub fn set(&mut self, flag: FeatureFlag, value: bool) {
self.values[flag.repr as usize].store(value, Ordering::SeqCst)
}
/// Parses a comma-separated feature-flag string, updating ourselves with the values.
/// Feature names or group names may be prefixed with "no-" to disable them.
/// The special group name "all" may be used for those who like to live on the edge.
/// Unknown features are silently ignored.
#[widestrs]
pub fn set_from_string<'a>(&mut self, str: impl Into<&'a wstr>) {
let str: &wstr = str.into();
let whitespace = "\t\n\0x0B\0x0C\r "L.as_char_slice();
for entry in str.as_char_slice().split(|c| *c == ',') {
if entry.is_empty() {
continue;
}
// Trim leading and trailing whitespace
let entry = &entry[entry.iter().take_while(|c| whitespace.contains(c)).count()..];
let entry =
&entry[..entry.len() - entry.iter().take_while(|c| whitespace.contains(c)).count()];
// A "no-" prefix inverts the sense.
let (name, value) = match entry.strip_prefix("no-"L.as_char_slice()) {
Some(suffix) => (suffix, false),
None => (entry, true),
};
// Look for a feature with this name. If we don't find it, assume it's a group name and set
// all features whose group contain it. Do nothing even if the string is unrecognized; this
// is to allow uniform invocations of fish (e.g. disable a feature that is only present in
// future versions).
// The special name 'all' may be used for those who like to live on the edge.
if let Some(md) = metadata.iter().find(|md| md.name == name) {
// Only change it if it's not read-only.
// Don't complain if it is, this is typically set from a variable.
if !md.read_only {
self.set(md.flag, value);
}
} else {
for md in &metadata {
if md.groups == name || name == "all"L {
if !md.read_only {
self.set(md.flag, value);
}
}
}
}
}
}
}
/// Return the global set of features for fish. This is const to prevent accidental mutation.
pub fn fish_features() -> *const Features {
unsafe { (*global_features).get() }
}
/// Perform a feature test on the global set of features.
pub fn feature_test(flag: FeatureFlag) -> bool {
unsafe { &*(*global_features).get() }.test(flag)
}
/// Return the global set of features for fish, but mutable. In general fish features should be set
/// at startup only.
pub fn mutable_fish_features() -> *mut Features {
unsafe { (*global_features).get() }
}
// The metadata, indexed by flag.
pub fn feature_metadata() -> [feature_metadata_t; metadata.len()] {
array::from_fn(|i| (&metadata[i]).into())
}
#[test]
#[widestrs]
fn test_feature_flags() {
let mut f = Features::new();
f.set_from_string("stderr-nocaret,nonsense"L);
assert!(f.test(FeatureFlag::stderr_nocaret));
f.set_from_string("stderr-nocaret,no-stderr-nocaret,nonsense"L);
assert!(f.test(FeatureFlag::stderr_nocaret));
// Ensure every metadata is represented once.
let mut counts: [usize; metadata.len()] = [0; metadata.len()];
for md in &metadata {
counts[md.flag.repr as usize] += 1;
}
for count in counts {
assert_eq!(count, 1);
}
assert_eq!(
metadata[FeatureFlag::stderr_nocaret.repr as usize].name,
"stderr-nocaret"L
);
}

View File

@ -1,352 +0,0 @@
use self::ffi::pgid_t;
use crate::common::{assert_send, assert_sync};
use crate::wchar_ffi::{WCharFromFFI, WCharToFFI};
use cxx::{CxxWString, UniquePtr};
use std::num::{NonZeroI32, NonZeroU32};
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::Mutex;
use widestring::WideUtfString;
#[cxx::bridge]
mod ffi {
// Not only does cxx bridge not recognize libc::pid_t, it doesn't even recognize i32 as a POD
// type! :sadface:
struct pgid_t {
value: i32,
}
extern "Rust" {
#[cxx_name = "job_group_t"]
type JobGroup;
fn wants_job_control(&self) -> bool;
fn wants_terminal(&self) -> bool;
fn is_foreground(&self) -> bool;
fn set_is_foreground(&self, value: bool);
#[cxx_name = "get_command"]
fn get_command_ffi(&self) -> UniquePtr<CxxWString>;
#[cxx_name = "get_job_id"]
fn get_job_id_ffi(&self) -> i32;
#[cxx_name = "get_cancel_signal"]
fn get_cancel_signal_ffi(&self) -> i32;
#[cxx_name = "cancel_with_signal"]
fn cancel_with_signal_ffi(&self, signal: i32);
fn set_pgid(&mut self, pgid: i32);
#[cxx_name = "get_pgid"]
fn get_pgid_ffi(&self) -> UniquePtr<pgid_t>;
fn has_job_id(&self) -> bool;
// cxx bridge doesn't recognize `libc::*` as being POD types, so it won't let us use them in
// a SharedPtr/UniquePtr/Box and won't let us pass/return them by value/reference, either.
unsafe fn get_modes_ffi(&self, size: usize) -> *const u8; /* actually `* const libc::termios` */
unsafe fn set_modes_ffi(&mut self, modes: *const u8, size: usize); /* actually `* const libc::termios` */
// The C++ code uses `shared_ptr<JobGroup>` but cxx bridge doesn't support returning a
// `SharedPtr<OpaqueRustType>` nor does it implement `Arc<T>` so we return a box and then
// convert `rust::box<T>` to `std::shared_ptr<T>` with `box_to_shared_ptr()` (from ffi.h).
fn create_job_group_ffi(command: &CxxWString, wants_job_id: bool) -> Box<JobGroup>;
fn create_job_group_with_job_control_ffi(
command: &CxxWString,
wants_term: bool,
) -> Box<JobGroup>;
}
}
fn create_job_group_ffi(command: &CxxWString, wants_job_id: bool) -> Box<JobGroup> {
let job_group = JobGroup::create(command.from_ffi(), wants_job_id);
Box::new(job_group)
}
fn create_job_group_with_job_control_ffi(command: &CxxWString, wants_term: bool) -> Box<JobGroup> {
let job_group = JobGroup::create_with_job_control(command.from_ffi(), wants_term);
Box::new(job_group)
}
/// A job id, corresponding to what is printed by `jobs`. 1 is the first valid job id.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct JobId(NonZeroU32);
/// `JobGroup` is conceptually similar to the idea of a process group. It represents data which
/// is shared among all of the "subjobs" that may be spawned by a single job.
/// For example, two fish functions in a pipeline may themselves spawn multiple jobs, but all will
/// share the same job group.
/// There is also a notion of a "internal" job group. Internal groups are used when executing a
/// foreground function or block with no pipeline. These are not jobs as the user understands them -
/// they do not consume a job id, they do not show up in job lists, and they do not have a pgid
/// because they contain no external procs. Note that `JobGroup` is intended to eventually be
/// shared between threads, and so must be thread safe.
#[derive(Debug)]
pub struct JobGroup {
/// If set, the saved terminal modes of this job. This needs to be saved so that we can restore
/// the terminal to the same state when resuming a stopped job.
pub tmodes: Option<libc::termios>,
/// Whether job control is enabled in this `JobGroup` or not.
///
/// If this is set, then the first process in the root job must be external, as it will become
/// the process group leader.
pub job_control: bool,
/// Whether we should `tcsetpgrp()` the job when it runs in the foreground. Should be checked
/// via [`Self::wants_terminal()`] only.
wants_term: bool,
/// Whether we are in the foreground, meaning the user is waiting for this job to complete.
pub is_foreground: AtomicBool,
/// The pgid leading our group. This is only ever set if [`job_control`](Self::JobControl) is
/// true. We ensure the value (when set) is always non-negative.
pgid: Option<libc::pid_t>,
/// The original command which produced this job tree.
pub command: WideUtfString,
/// Our job id, if any. `None` here should evaluate to `-1` for ffi purposes.
/// "Simple block" groups like function calls do not have a job id.
pub job_id: Option<JobId>,
/// The signal causing the group to cancel or `0` if none.
/// Not using an `Option<NonZeroI32>` to be able to atomically load/store to this field.
signal: AtomicI32,
}
const _: () = assert_send::<JobGroup>();
const _: () = assert_sync::<JobGroup>();
impl JobGroup {
/// Whether this job wants job control.
pub fn wants_job_control(&self) -> bool {
self.job_control
}
/// If this job should own the terminal when it runs. True only if both [`Self::wants_term]` and
/// [`Self::is_foreground`] are true.
pub fn wants_terminal(&self) -> bool {
self.wants_term && self.is_foreground()
}
/// Whether we are the currently the foreground group. Should never be true for more than one
/// `JobGroup` at any given moment.
pub fn is_foreground(&self) -> bool {
self.is_foreground.load(Ordering::Relaxed)
}
/// Mark whether we are in the foreground.
pub fn set_is_foreground(&self, in_foreground: bool) {
self.is_foreground.store(in_foreground, Ordering::Relaxed);
}
/// Return the command which produced this job tree.
pub fn get_command_ffi(&self) -> UniquePtr<CxxWString> {
self.command.to_ffi()
}
/// Return the job id or -1 if none.
pub fn get_job_id_ffi(&self) -> i32 {
self.job_id.map(|j| u32::from(j.0) as i32).unwrap_or(-1)
}
/// Returns whether we have valid job id. "Simple block" groups like function calls do not.
pub fn has_job_id(&self) -> bool {
self.job_id.is_some()
}
/// Gets the cancellation signal, if any.
pub fn get_cancel_signal(&self) -> Option<NonZeroI32> {
match self.signal.load(Ordering::Relaxed) {
0 => None,
s => Some(NonZeroI32::new(s).unwrap()),
}
}
/// Gets the cancellation signal or `0` if none.
pub fn get_cancel_signal_ffi(&self) -> i32 {
// Legacy C++ code expects a zero in case of no signal.
self.get_cancel_signal().map(|s| s.into()).unwrap_or(0)
}
/// Mark that a process in this group got a signal and should cancel.
pub fn cancel_with_signal(&self, signal: NonZeroI32) {
// We only assign the signal if one hasn't yet been assigned. This means the first signal to
// register wins over any that come later.
self.signal
.compare_exchange(0, signal.into(), Ordering::Relaxed, Ordering::Relaxed)
.ok();
}
/// Mark that a process in this group got a signal and should cancel
pub fn cancel_with_signal_ffi(&self, signal: i32) {
self.cancel_with_signal(signal.try_into().expect("Invalid zero signal!"));
}
/// Set the pgid for this job group, latching it to this value. This should only be called if
/// job control is active for this group. The pgid should not already have been set, and should
/// be different from fish's pgid. Of course this does not keep the pgid alive by itself.
///
/// Note we need not be concerned about thread safety. job_groups are intended to be shared
/// across threads, but any pgid should always have been set beforehand, since it's set
/// immediately after the first process launches.
///
/// As such, this method takes `&mut self` rather than `&self` to enforce that this operation is
/// only available during initial construction/initialization.
pub fn set_pgid(&mut self, pgid: libc::pid_t) {
assert!(
self.wants_job_control(),
"Should not set a pgid for a group that doesn't want job control!"
);
assert!(pgid >= 0, "Invalid pgid!");
assert!(self.pgid.is_none(), "JobGroup::pgid already set!");
self.pgid = Some(pgid);
}
/// Returns the value of [`JobGroup::pgid`]. This is never fish's own pgid!
pub fn get_pgid(&self) -> Option<libc::pid_t> {
self.pgid
}
/// Returns the value of [`JobGroup::pgid`] in a `UniquePtr<T>` to take the place of an
/// `Option<T>` for ffi purposes. A null `UniquePtr` is equivalent to `None`.
pub fn get_pgid_ffi(&self) -> cxx::UniquePtr<pgid_t> {
match self.pgid {
Some(value) => UniquePtr::new(pgid_t { value }),
None => UniquePtr::null(),
}
}
/// Returns the current terminal modes associated with the `JobGroup` for ffi purposes.
unsafe fn get_modes_ffi(&self, size: usize) -> *const u8 {
assert_eq!(
size,
core::mem::size_of::<libc::termios>(),
"Mismatch between expected and actual ffi size of struct termios!"
);
self.tmodes
.as_ref()
// Really cool that type inference works twice in a row here. The first `_` is deduced
// from the left and the second `_` is deduced from the right (the return type).
.map(|val| val as *const _ as *const _)
.unwrap_or(core::ptr::null())
}
/// Sets the current terminal modes associated with the `JobGroup`. Only use for ffi.
///
/// Unlike `set_pgid()`, this isn't documented in the C++ codebase as being only called at
/// initialization but as the underlying [`self.tmodes`] wasn't wrapped in any sort of
/// thread-safe marshalling struct, we'll assume it can only be called from one thread and use
/// `&mut self` for safety.
unsafe fn set_modes_ffi(&mut self, modes: *const u8, size: usize) {
assert_eq!(
size,
core::mem::size_of::<libc::termios>(),
"Mismatch between expected and actual ffi size of struct termios!"
);
let modes = modes as *const libc::termios;
if modes.is_null() {
self.tmodes = None;
} else {
self.tmodes = Some(*modes);
}
}
}
/// Basic thread-safe sorted vector of job ids currently in use.
///
/// In the C++ codebase, this is deliberately leaked to avoid destructor ordering issues - see
/// #6539. Rust automatically "leaks" all `static` variables (does not call their `Drop` impls)
/// because of the inherent difficulty in doing that correctly (i.e. what we ran into).
static CONSUMED_JOB_IDS: Mutex<Vec<JobId>> = Mutex::new(Vec::new());
impl JobId {
const NONE: Option<JobId> = None;
/// Return a `JobId` that is greater than all extant job ids stored in [`CONSUMED_JOB_IDS`].
/// The `JobId` should be freed with [`JobId::release()`] when it is no longer in use.
fn acquire() -> Option<Self> {
let mut consumed_job_ids = CONSUMED_JOB_IDS.lock().expect("Poisoned mutex!");
// The new job id should be greater than the largest currently used id (#6053). The job ids
// in CONSUMED_JOB_IDS are sorted in ascending order, so we just have to check the last.
let job_id = consumed_job_ids
.last()
.map(JobId::next)
.unwrap_or(JobId(1.try_into().unwrap()));
consumed_job_ids.push(job_id);
return Some(job_id);
}
/// Remove the provided `JobId` from [`CONSUMED_JOB_IDS`].
fn release(id: JobId) {
let mut consumed_job_ids = CONSUMED_JOB_IDS.lock().expect("Poisoned mutex!");
let pos = consumed_job_ids
.binary_search(&id)
.expect("Job id was not in use!");
consumed_job_ids.remove(pos);
}
/// Increments the internal id and returns it wrapped in a new `JobId`.
fn next(&self) -> JobId {
JobId(self.0.checked_add(1).expect("Job id overflow!"))
}
}
impl JobGroup {
pub fn new(
command: WideUtfString,
id: Option<JobId>,
job_control: bool,
wants_term: bool,
) -> Self {
// We *can* have a job id without job control, but not the reverse.
if job_control {
assert!(id.is_some(), "Cannot have job control without a job id!");
}
if wants_term {
assert!(job_control, "Cannot take terminal without job control!");
}
Self {
job_id: id,
job_control,
wants_term,
command,
tmodes: None,
signal: 0.into(),
is_foreground: false.into(),
pgid: None,
}
}
/// Return a new `JobGroup` with the provided `command`. The `JobGroup` is only assigned a
/// `JobId` if `wants_job_id` is true and is created with job control disabled and
/// [`JobGroup::wants_term`] set to false.
pub fn create(command: WideUtfString, wants_job_id: bool) -> JobGroup {
JobGroup::new(
command,
if wants_job_id {
JobId::acquire()
} else {
JobId::NONE
},
false, /* job_control */
false, /* wants_term */
)
}
/// Return a new `JobGroup` with the provided `command` with job control enabled. A [`JobId`] is
/// automatically acquired and assigned. If `wants_term` is true then [`JobGroup::wants_term`]
/// is also set to `true` accordingly.
pub fn create_with_job_control(command: WideUtfString, wants_term: bool) -> JobGroup {
JobGroup::new(
command,
JobId::acquire(),
true, /* job_control */
wants_term,
)
}
}
impl Drop for JobGroup {
fn drop(&mut self) {
if let Some(job_id) = self.job_id {
JobId::release(job_id);
}
}
}

View File

@ -1,53 +0,0 @@
#![allow(non_camel_case_types)]
#![allow(dead_code)]
#![allow(non_upper_case_globals)]
#![allow(clippy::needless_return)]
#![allow(clippy::manual_is_ascii_check)]
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::derivable_impls)]
#[macro_use]
mod common;
mod color;
mod event;
mod fd_monitor;
mod fd_readable_set;
mod fds;
#[allow(rustdoc::broken_intra_doc_links)]
#[allow(clippy::module_inception)]
#[allow(clippy::new_ret_no_self)]
#[allow(clippy::wrong_self_convention)]
#[allow(clippy::needless_lifetimes)]
mod ffi;
mod ffi_init;
mod ffi_tests;
mod flog;
mod future_feature_flags;
mod job_group;
mod nix;
mod parse_constants;
mod redirection;
mod signal;
mod smoke;
mod threads;
mod timer;
mod tokenizer;
mod topic_monitor;
mod util;
mod wchar;
mod wchar_ext;
mod wchar_ffi;
mod wgetopt;
mod wutil;
mod abbrs;
mod builtins;
mod env;
mod re;
mod expand;
mod path;
// Don't use `#[cfg(test)]` here to make sure ffi tests are built and tested
mod tests;

View File

@ -1,24 +0,0 @@
//! Safe wrappers around various libc functions that we might want to reuse across modules.
use std::time::Duration;
#[allow(clippy::unnecessary_cast)]
pub const fn timeval_to_duration(val: &libc::timeval) -> Duration {
let micros = val.tv_sec as i64 * (1E6 as i64) + val.tv_usec as i64;
Duration::from_micros(micros as u64)
}
pub trait TimevalExt {
fn as_micros(&self) -> i64;
fn as_duration(&self) -> Duration;
}
impl TimevalExt for libc::timeval {
fn as_micros(&self) -> i64 {
timeval_to_duration(self).as_micros() as i64
}
fn as_duration(&self) -> Duration {
timeval_to_duration(self)
}
}

View File

@ -1,751 +0,0 @@
//! Constants used in the programmatic representation of fish code.
use crate::ffi::{fish_wcswidth, fish_wcwidth, wcharz_t};
use crate::tokenizer::variable_assignment_equals_pos;
use crate::wchar::{wstr, WString, L};
use crate::wchar_ffi::{wcharz, WCharFromFFI, WCharToFFI};
use crate::wutil::{sprintf, wgettext_fmt};
use cxx::{CxxWString, UniquePtr};
use std::ops::{BitAnd, BitOr, BitOrAssign};
use widestring_suffix::widestrs;
pub type SourceOffset = u32;
pub const SOURCE_OFFSET_INVALID: SourceOffset = SourceOffset::MAX;
pub const SOURCE_LOCATION_UNKNOWN: usize = usize::MAX;
#[derive(Copy, Clone)]
pub struct ParseTreeFlags(pub u8);
pub const PARSE_FLAG_NONE: ParseTreeFlags = ParseTreeFlags(0);
/// attempt to build a "parse tree" no matter what. this may result in a 'forest' of
/// disconnected trees. this is intended to be used by syntax highlighting.
pub const PARSE_FLAG_CONTINUE_AFTER_ERROR: ParseTreeFlags = ParseTreeFlags(1 << 0);
/// include comment tokens.
pub const PARSE_FLAG_INCLUDE_COMMENTS: ParseTreeFlags = ParseTreeFlags(1 << 1);
/// indicate that the tokenizer should accept incomplete tokens */
pub const PARSE_FLAG_ACCEPT_INCOMPLETE_TOKENS: ParseTreeFlags = ParseTreeFlags(1 << 2);
/// indicate that the parser should not generate the terminate token, allowing an 'unfinished'
/// tree where some nodes may have no productions.
pub const PARSE_FLAG_LEAVE_UNTERMINATED: ParseTreeFlags = ParseTreeFlags(1 << 3);
/// indicate that the parser should generate job_list entries for blank lines.
pub const PARSE_FLAG_SHOW_BLANK_LINES: ParseTreeFlags = ParseTreeFlags(1 << 4);
/// indicate that extra semis should be generated.
pub const PARSE_FLAG_SHOW_EXTRA_SEMIS: ParseTreeFlags = ParseTreeFlags(1 << 5);
impl BitAnd for ParseTreeFlags {
type Output = bool;
fn bitand(self, rhs: Self) -> Self::Output {
(self.0 & rhs.0) != 0
}
}
impl BitOr for ParseTreeFlags {
type Output = ParseTreeFlags;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign for ParseTreeFlags {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0
}
}
#[derive(PartialEq, Eq, Copy, Clone)]
pub struct ParserTestErrorBits(u8);
pub const PARSER_TEST_ERROR: ParserTestErrorBits = ParserTestErrorBits(1);
pub const PARSER_TEST_INCOMPLETE: ParserTestErrorBits = ParserTestErrorBits(2);
impl BitAnd for ParserTestErrorBits {
type Output = bool;
fn bitand(self, rhs: Self) -> Self::Output {
(self.0 & rhs.0) != 0
}
}
impl BitOrAssign for ParserTestErrorBits {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0
}
}
#[cxx::bridge]
mod parse_constants_ffi {
extern "C++" {
include!("wutil.h");
type wcharz_t = super::wcharz_t;
}
/// A range of source code.
#[derive(PartialEq, Eq, Clone, Copy)]
struct SourceRange {
start: u32,
length: u32,
}
extern "Rust" {
fn end(self: &SourceRange) -> u32;
fn contains_inclusive(self: &SourceRange, loc: u32) -> bool;
}
/// IMPORTANT: If the following enum table is modified you must also update token_type_description below.
/// TODO above comment can be removed when we drop the FFI and get real enums.
#[derive(Clone, Copy)]
enum ParseTokenType {
invalid = 1,
// Terminal types.
string,
pipe,
redirection,
background,
andand,
oror,
end,
// Special terminal type that means no more tokens forthcoming.
terminate,
// Very special terminal types that don't appear in the production list.
error,
tokenizer_error,
comment,
}
#[repr(u8)]
#[derive(Clone, Copy)]
enum ParseKeyword {
// 'none' is not a keyword, it is a sentinel indicating nothing.
none,
kw_and,
kw_begin,
kw_builtin,
kw_case,
kw_command,
kw_else,
kw_end,
kw_exclam,
kw_exec,
kw_for,
kw_function,
kw_if,
kw_in,
kw_not,
kw_or,
kw_switch,
kw_time,
kw_while,
}
extern "Rust" {
fn token_type_description(token_type: ParseTokenType) -> wcharz_t;
fn keyword_description(keyword: ParseKeyword) -> wcharz_t;
fn keyword_from_string(s: wcharz_t) -> ParseKeyword;
}
// Statement decorations like 'command' or 'exec'.
pub enum StatementDecoration {
none,
command,
builtin,
exec,
}
// Parse error code list.
pub enum ParseErrorCode {
none,
// Matching values from enum parser_error.
syntax,
cmdsubst,
generic, // unclassified error types
// Tokenizer errors.
tokenizer_unterminated_quote,
tokenizer_unterminated_subshell,
tokenizer_unterminated_slice,
tokenizer_unterminated_escape,
tokenizer_other,
unbalancing_end, // end outside of block
unbalancing_else, // else outside of if
unbalancing_case, // case outside of switch
bare_variable_assignment, // a=b without command
andor_in_pipeline, // "and" or "or" after a pipe
}
struct parse_error_t {
text: UniquePtr<CxxWString>,
code: ParseErrorCode,
source_start: usize,
source_length: usize,
}
extern "Rust" {
type ParseError;
fn code(self: &ParseError) -> ParseErrorCode;
fn source_start(self: &ParseError) -> usize;
fn text(self: &ParseError) -> UniquePtr<CxxWString>;
#[cxx_name = "describe"]
fn describe_ffi(
self: &ParseError,
src: &CxxWString,
is_interactive: bool,
) -> UniquePtr<CxxWString>;
#[cxx_name = "describe_with_prefix"]
fn describe_with_prefix_ffi(
self: &ParseError,
src: &CxxWString,
prefix: &CxxWString,
is_interactive: bool,
skip_caret: bool,
) -> UniquePtr<CxxWString>;
fn describe_with_prefix(
self: &parse_error_t,
src: &CxxWString,
prefix: &CxxWString,
is_interactive: bool,
skip_caret: bool,
) -> UniquePtr<CxxWString>;
type ParseErrorList;
fn new_parse_error_list() -> Box<ParseErrorList>;
#[cxx_name = "offset_source_start"]
fn offset_source_start_ffi(self: &mut ParseErrorList, amt: usize);
fn size(self: &ParseErrorList) -> usize;
fn at(self: &ParseErrorList, offset: usize) -> *const ParseError;
fn empty(self: &ParseErrorList) -> bool;
fn push_back(self: &mut ParseErrorList, error: &parse_error_t);
fn append(self: &mut ParseErrorList, other: *mut ParseErrorList);
fn erase(self: &mut ParseErrorList, index: usize);
fn clear(self: &mut ParseErrorList);
}
extern "Rust" {
#[cxx_name = "token_type_user_presentable_description"]
fn token_type_user_presentable_description_ffi(
type_: ParseTokenType,
keyword: ParseKeyword,
) -> UniquePtr<CxxWString>;
}
// The location of a pipeline.
enum PipelinePosition {
none, // not part of a pipeline
first, // first command in a pipeline
subsequent, // second or further command in a pipeline
}
}
pub use parse_constants_ffi::{
parse_error_t, ParseErrorCode, ParseKeyword, ParseTokenType, SourceRange, StatementDecoration,
};
impl SourceRange {
pub fn new(start: SourceOffset, length: SourceOffset) -> Self {
SourceRange { start, length }
}
pub fn end(&self) -> SourceOffset {
self.start.checked_add(self.length).expect("Overflow")
}
// \return true if a location is in this range, including one-past-the-end.
pub fn contains_inclusive(&self, loc: SourceOffset) -> bool {
self.start <= loc && loc - self.start <= self.length
}
}
impl Default for ParseTokenType {
fn default() -> Self {
ParseTokenType::invalid
}
}
impl From<ParseTokenType> for &'static wstr {
#[widestrs]
fn from(token_type: ParseTokenType) -> Self {
match token_type {
ParseTokenType::comment => "ParseTokenType::comment"L,
ParseTokenType::error => "ParseTokenType::error"L,
ParseTokenType::tokenizer_error => "ParseTokenType::tokenizer_error"L,
ParseTokenType::background => "ParseTokenType::background"L,
ParseTokenType::end => "ParseTokenType::end"L,
ParseTokenType::pipe => "ParseTokenType::pipe"L,
ParseTokenType::redirection => "ParseTokenType::redirection"L,
ParseTokenType::string => "ParseTokenType::string"L,
ParseTokenType::andand => "ParseTokenType::andand"L,
ParseTokenType::oror => "ParseTokenType::oror"L,
ParseTokenType::terminate => "ParseTokenType::terminate"L,
ParseTokenType::invalid => "ParseTokenType::invalid"L,
_ => "unknown token type"L,
}
}
}
fn token_type_description(token_type: ParseTokenType) -> wcharz_t {
let s: &'static wstr = token_type.into();
wcharz!(s)
}
impl Default for ParseKeyword {
fn default() -> Self {
ParseKeyword::none
}
}
impl From<ParseKeyword> for &'static wstr {
#[widestrs]
fn from(keyword: ParseKeyword) -> Self {
match keyword {
ParseKeyword::kw_exclam => "!"L,
ParseKeyword::kw_and => "and"L,
ParseKeyword::kw_begin => "begin"L,
ParseKeyword::kw_builtin => "builtin"L,
ParseKeyword::kw_case => "case"L,
ParseKeyword::kw_command => "command"L,
ParseKeyword::kw_else => "else"L,
ParseKeyword::kw_end => "end"L,
ParseKeyword::kw_exec => "exec"L,
ParseKeyword::kw_for => "for"L,
ParseKeyword::kw_function => "function"L,
ParseKeyword::kw_if => "if"L,
ParseKeyword::kw_in => "in"L,
ParseKeyword::kw_not => "not"L,
ParseKeyword::kw_or => "or"L,
ParseKeyword::kw_switch => "switch"L,
ParseKeyword::kw_time => "time"L,
ParseKeyword::kw_while => "while"L,
_ => "unknown_keyword"L,
}
}
}
fn keyword_description(keyword: ParseKeyword) -> wcharz_t {
let s: &'static wstr = keyword.into();
wcharz!(s)
}
impl From<&wstr> for ParseKeyword {
fn from(s: &wstr) -> Self {
let s: Vec<u8> = s.encode_utf8().collect();
match unsafe { std::str::from_utf8_unchecked(&s) } {
"!" => ParseKeyword::kw_exclam,
"and" => ParseKeyword::kw_and,
"begin" => ParseKeyword::kw_begin,
"builtin" => ParseKeyword::kw_builtin,
"case" => ParseKeyword::kw_case,
"command" => ParseKeyword::kw_command,
"else" => ParseKeyword::kw_else,
"end" => ParseKeyword::kw_end,
"exec" => ParseKeyword::kw_exec,
"for" => ParseKeyword::kw_for,
"function" => ParseKeyword::kw_function,
"if" => ParseKeyword::kw_if,
"in" => ParseKeyword::kw_in,
"not" => ParseKeyword::kw_not,
"or" => ParseKeyword::kw_or,
"switch" => ParseKeyword::kw_switch,
"time" => ParseKeyword::kw_time,
"while" => ParseKeyword::kw_while,
_ => ParseKeyword::none,
}
}
}
fn keyword_from_string<'a>(s: impl Into<&'a wstr>) -> ParseKeyword {
let s: &wstr = s.into();
ParseKeyword::from(s)
}
#[derive(Clone)]
pub struct ParseError {
/// Text of the error.
pub text: WString,
/// Code for the error.
pub code: ParseErrorCode,
/// Offset and length of the token in the source code that triggered this error.
pub source_start: usize,
pub source_length: usize,
}
impl Default for ParseError {
fn default() -> ParseError {
ParseError {
text: L!("").to_owned(),
code: ParseErrorCode::none,
source_start: 0,
source_length: 0,
}
}
}
impl ParseError {
/// Return a string describing the error, suitable for presentation to the user. If
/// is_interactive is true, the offending line with a caret is printed as well.
pub fn describe(self: &ParseError, src: &wstr, is_interactive: bool) -> WString {
self.describe_with_prefix(src, L!(""), is_interactive, false)
}
/// Return a string describing the error, suitable for presentation to the user, with the given
/// prefix. If skip_caret is false, the offending line with a caret is printed as well.
pub fn describe_with_prefix(
self: &ParseError,
src: &wstr,
prefix: &wstr,
is_interactive: bool,
skip_caret: bool,
) -> WString {
let mut result = prefix.to_owned();
// Some errors don't have their message passed in, so we construct them here.
// This affects e.g. `eval "a=(foo)"`
match self.code {
ParseErrorCode::andor_in_pipeline => {
let context = wstr::from_char_slice(
&src.as_char_slice()[self.source_start..self.source_start + self.source_length],
);
result += wstr::from_char_slice(
wgettext_fmt!(INVALID_PIPELINE_CMD_ERR_MSG, context).as_char_slice(),
);
}
ParseErrorCode::bare_variable_assignment => {
let context = wstr::from_char_slice(
&src.as_char_slice()[self.source_start..self.source_start + self.source_length],
);
let assignment_src = context;
#[allow(clippy::explicit_auto_deref)]
let equals_pos = variable_assignment_equals_pos(assignment_src).unwrap();
let variable = &assignment_src[..equals_pos];
let value = &assignment_src[equals_pos + 1..];
result += wstr::from_char_slice(
wgettext_fmt!(ERROR_BAD_COMMAND_ASSIGN_ERR_MSG, variable, value)
.as_char_slice(),
);
}
_ => {
if skip_caret && self.text.is_empty() {
return L!("").to_owned();
}
result += wstr::from_char_slice(self.text.as_char_slice());
}
}
let mut start = self.source_start;
let mut len = self.source_length;
if start >= src.len() {
// If we are past the source, we clamp it to the end.
start = src.len() - 1;
len = 0;
}
if start + len > src.len() {
len = src.len() - self.source_start;
}
if skip_caret {
return result;
}
// Locate the beginning of this line of source.
let mut line_start = 0;
// Look for a newline prior to source_start. If we don't find one, start at the beginning of
// the string; otherwise start one past the newline. Note that source_start may itself point
// at a newline; we want to find the newline before it.
if start > 0 {
let prefix = &src.as_char_slice()[..start];
let newline_left_of_start = prefix.iter().rev().position(|c| *c == '\n');
if let Some(left_of_start) = newline_left_of_start {
line_start = start - left_of_start;
}
}
// Look for the newline after the source range. If the source range itself includes a
// newline, that's the one we want, so start just before the end of the range.
let last_char_in_range = if len == 0 { start } else { start + len - 1 };
let line_end = src.as_char_slice()[last_char_in_range..]
.iter()
.position(|c| *c == '\n')
.map(|pos| pos + last_char_in_range)
.unwrap_or(src.len());
assert!(line_end >= line_start);
assert!(start >= line_start);
// Don't include the caret and line if we're interactive and this is the first line, because
// then it's obvious.
let interactive_skip_caret = is_interactive && start == 0;
if interactive_skip_caret {
return result;
}
// Append the line of text.
if !result.is_empty() {
result += "\n";
}
result += wstr::from_char_slice(&src.as_char_slice()[line_start..line_end]);
// Append the caret line. The input source may include tabs; for that reason we
// construct a "caret line" that has tabs in corresponding positions.
let mut caret_space_line = WString::new();
caret_space_line.reserve(start - line_start);
for i in line_start..start {
let wc = src.as_char_slice()[i];
if wc == '\t' {
caret_space_line += "\t";
} else if wc == '\n' {
// It's possible that the start points at a newline itself. In that case,
// pretend it's a space. We only expect this to be at the end of the string.
caret_space_line += " ";
} else {
let width = fish_wcwidth(wc.into()).0;
if width > 0 {
caret_space_line += " ".repeat(width as usize).as_str();
}
}
}
result += "\n";
result += wstr::from_char_slice(caret_space_line.as_char_slice());
result += "^";
if len > 1 {
// Add a squiggle under the error location.
// We do it like this
// ^~~^
// With a "^" under the start and end, and squiggles in-between.
let width = fish_wcswidth(unsafe { src.as_ptr().add(start) }, len).0;
if width >= 2 {
// Subtract one for each of the carets - this is important in case
// the starting char has a width of > 1.
result += "~".repeat(width as usize - 2).as_str();
result += "^";
}
}
result
}
}
impl From<&parse_error_t> for ParseError {
fn from(error: &parse_error_t) -> Self {
ParseError {
text: error.text.from_ffi(),
code: error.code,
source_start: error.source_start,
source_length: error.source_length,
}
}
}
impl parse_error_t {
fn describe_with_prefix(
self: &parse_error_t,
src: &CxxWString,
prefix: &CxxWString,
is_interactive: bool,
skip_caret: bool,
) -> UniquePtr<CxxWString> {
ParseError::from(self).describe_with_prefix_ffi(src, prefix, is_interactive, skip_caret)
}
}
impl ParseError {
fn code(&self) -> ParseErrorCode {
self.code
}
fn source_start(&self) -> usize {
self.source_start
}
fn text(&self) -> UniquePtr<CxxWString> {
self.text.to_ffi()
}
fn describe_ffi(
self: &ParseError,
src: &CxxWString,
is_interactive: bool,
) -> UniquePtr<CxxWString> {
self.describe(&src.from_ffi(), is_interactive).to_ffi()
}
fn describe_with_prefix_ffi(
self: &ParseError,
src: &CxxWString,
prefix: &CxxWString,
is_interactive: bool,
skip_caret: bool,
) -> UniquePtr<CxxWString> {
self.describe_with_prefix(
&src.from_ffi(),
&prefix.from_ffi(),
is_interactive,
skip_caret,
)
.to_ffi()
}
}
#[widestrs]
pub fn token_type_user_presentable_description(
type_: ParseTokenType,
keyword: ParseKeyword,
) -> WString {
if keyword != ParseKeyword::none {
return sprintf!("keyword: '%ls'"L, Into::<&'static wstr>::into(keyword));
}
match type_ {
ParseTokenType::string => "a string"L.to_owned(),
ParseTokenType::pipe => "a pipe"L.to_owned(),
ParseTokenType::redirection => "a redirection"L.to_owned(),
ParseTokenType::background => "a '&'"L.to_owned(),
ParseTokenType::andand => "'&&'"L.to_owned(),
ParseTokenType::oror => "'||'"L.to_owned(),
ParseTokenType::end => "end of the statement"L.to_owned(),
ParseTokenType::terminate => "end of the input"L.to_owned(),
ParseTokenType::error => "a parse error"L.to_owned(),
ParseTokenType::tokenizer_error => "an incomplete token"L.to_owned(),
ParseTokenType::comment => "a comment"L.to_owned(),
_ => sprintf!("a %ls"L, Into::<&'static wstr>::into(type_)),
}
}
fn token_type_user_presentable_description_ffi(
type_: ParseTokenType,
keyword: ParseKeyword,
) -> UniquePtr<CxxWString> {
token_type_user_presentable_description(type_, keyword).to_ffi()
}
/// TODO This should be type alias once we drop the FFI.
pub struct ParseErrorList(pub Vec<ParseError>);
/// Helper function to offset error positions by the given amount. This is used when determining
/// errors in a substring of a larger source buffer.
pub fn parse_error_offset_source_start(errors: &mut ParseErrorList, amt: usize) {
if amt > 0 {
for ref mut error in errors.0.iter_mut() {
// Preserve the special meaning of -1 as 'unknown'.
if error.source_start != SOURCE_LOCATION_UNKNOWN {
error.source_start += amt;
}
}
}
}
fn new_parse_error_list() -> Box<ParseErrorList> {
Box::new(ParseErrorList(Vec::new()))
}
impl ParseErrorList {
fn offset_source_start_ffi(&mut self, amt: usize) {
parse_error_offset_source_start(self, amt)
}
fn size(&self) -> usize {
self.0.len()
}
fn at(&self, offset: usize) -> *const ParseError {
&self.0[offset]
}
fn empty(&self) -> bool {
self.0.is_empty()
}
fn push_back(&mut self, error: &parse_error_t) {
self.0.push(error.into())
}
fn append(&mut self, other: *mut ParseErrorList) {
self.0.append(&mut (unsafe { &*other }.0.clone()));
}
fn erase(&mut self, index: usize) {
self.0.remove(index);
}
fn clear(&mut self) {
self.0.clear()
}
}
/// Maximum number of function calls.
pub const FISH_MAX_STACK_DEPTH: usize = 128;
/// Maximum number of nested string substitutions (in lieu of evals)
/// Reduced under TSAN: our CI test creates 500 jobs and this is very slow with TSAN.
#[cfg(feature = "FISH_TSAN_WORKAROUNDS")]
pub const FISH_MAX_EVAL_DEPTH: usize = 250;
#[cfg(not(feature = "FISH_TSAN_WORKAROUNDS"))]
pub const FISH_MAX_EVAL_DEPTH: usize = 500;
/// Error message on a function that calls itself immediately.
pub const INFINITE_FUNC_RECURSION_ERR_MSG: &str =
"The function '%ls' calls itself immediately, which would result in an infinite loop.";
/// Error message on reaching maximum call stack depth.
pub const CALL_STACK_LIMIT_EXCEEDED_ERR_MSG: &str =
"The call stack limit has been exceeded. Do you have an accidental infinite loop?";
/// Error message when encountering an unknown builtin name.
pub const UNKNOWN_BUILTIN_ERR_MSG: &str = "Unknown builtin '%ls'";
/// Error message when encountering a failed expansion, e.g. for the variable name in for loops.
pub const FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG: &str = "Unable to expand variable name '%ls'";
/// Error message when encountering an illegal file descriptor.
pub const ILLEGAL_FD_ERR_MSG: &str = "Illegal file descriptor in redirection '%ls'";
/// Error message for wildcards with no matches.
pub const WILDCARD_ERR_MSG: &str = "No matches for wildcard '%ls'. See `help wildcards-globbing`.";
/// Error when using break outside of loop.
pub const INVALID_BREAK_ERR_MSG: &str = "'break' while not inside of loop";
/// Error when using continue outside of loop.
pub const INVALID_CONTINUE_ERR_MSG: &str = "'continue' while not inside of loop";
/// Error message when a command may not be in a pipeline.
pub const INVALID_PIPELINE_CMD_ERR_MSG: &str = "The '%ls' command can not be used in a pipeline";
// Error messages. The number is a reminder of how many format specifiers are contained.
/// Error for $^.
pub const ERROR_BAD_VAR_CHAR1: &str = "$%lc is not a valid variable in fish.";
/// Error for ${a}.
pub const ERROR_BRACKETED_VARIABLE1: &str =
"Variables cannot be bracketed. In fish, please use {$%ls}.";
/// Error for "${a}".
pub const ERROR_BRACKETED_VARIABLE_QUOTED1: &str =
"Variables cannot be bracketed. In fish, please use \"$%ls\".";
/// Error issued on $?.
pub const ERROR_NOT_STATUS: &str = "$? is not the exit status. In fish, please use $status.";
/// Error issued on $$.
pub const ERROR_NOT_PID: &str = "$$ is not the pid. In fish, please use $fish_pid.";
/// Error issued on $#.
pub const ERROR_NOT_ARGV_COUNT: &str = "$# is not supported. In fish, please use 'count $argv'.";
/// Error issued on $@.
pub const ERROR_NOT_ARGV_AT: &str = "$@ is not supported. In fish, please use $argv.";
/// Error issued on $*.
pub const ERROR_NOT_ARGV_STAR: &str = "$* is not supported. In fish, please use $argv.";
/// Error issued on $.
pub const ERROR_NO_VAR_NAME: &str = "Expected a variable name after this $.";
/// Error message for Posix-style assignment: foo=bar.
pub const ERROR_BAD_COMMAND_ASSIGN_ERR_MSG: &str =
"Unsupported use of '='. In fish, please use 'set %ls %ls'.";
/// Error message for a command like `time foo &`.
pub const ERROR_TIME_BACKGROUND: &str =
"'time' is not supported for background jobs. Consider using 'command time'.";
/// Error issued on { echo; echo }.
pub const ERROR_NO_BRACE_GROUPING: &str =
"'{ ... }' is not supported for grouping commands. Please use 'begin; ...; end'";

View File

@ -1,55 +0,0 @@
use crate::{
expand::ExpandChars::HomeDirectory,
wchar::{wstr, WExt, WString, L},
};
/// If the given path looks like it's relative to the working directory, then prepend that working
/// directory. This operates on unescaped paths only (so a ~ means a literal ~).
pub fn path_apply_working_directory(path: &wstr, working_directory: &wstr) -> WString {
if path.is_empty() || working_directory.is_empty() {
return path.to_owned();
}
// We're going to make sure that if we want to prepend the wd, that the string has no leading
// "/".
let prepend_wd = path.char_at(0) != '/' && path.char_at(0) != HomeDirectory.into();
if !prepend_wd {
// No need to prepend the wd, so just return the path we were given.
return path.to_owned();
}
// Remove up to one "./".
let mut path_component = path.to_owned();
if path_component.starts_with("./") {
path_component.replace_range(0..2, L!(""));
}
// Removing leading /s.
while path_component.starts_with("/") {
path_component.replace_range(0..1, L!(""));
}
// Construct and return a new path.
let mut new_path = working_directory.to_owned();
append_path_component(&mut new_path, &path_component);
new_path
}
pub fn append_path_component(path: &mut WString, component: &wstr) {
if path.is_empty() || component.is_empty() {
path.push_utfstr(component);
} else {
let path_len = path.len();
let path_slash = path.char_at(path_len - 1) == '/';
let comp_slash = component.as_char_slice()[0] == '/';
if !path_slash && !comp_slash {
// Need a slash
path.push('/');
} else if path_slash && comp_slash {
// Too many slashes.
path.pop();
}
path.push_utfstr(component);
}
}

View File

@ -1,46 +0,0 @@
use crate::wchar::{wstr, WString, L};
/// Adjust a pattern so that it is anchored at both beginning and end.
/// This is a workaround for the fact that PCRE2_ENDANCHORED is unavailable on pre-2017 PCRE2
/// (e.g. 10.21, on Xenial).
pub fn regex_make_anchored(pattern: &wstr) -> WString {
let mut anchored = pattern.to_owned();
// PATTERN -> ^(:?PATTERN)$.
let prefix = L!("^(?:");
let suffix = L!(")$");
anchored.reserve(pattern.len() + prefix.len() + suffix.len());
anchored.insert_utfstr(0, prefix);
anchored.push_utfstr(suffix);
anchored
}
use crate::ffi_tests::add_test;
add_test!("test_regex_make_anchored", || {
use crate::ffi;
use crate::wchar::L;
use crate::wchar_ffi::WCharToFFI;
let flags = ffi::re::flags_t { icase: false };
let mut result = ffi::try_compile(&regex_make_anchored(L!("ab(.+?)")), &flags);
assert!(!result.has_error());
let re = result.as_mut().get_regex();
assert!(!re.is_null());
assert!(!re.matches_ffi(&L!("").to_ffi()));
assert!(!re.matches_ffi(&L!("ab").to_ffi()));
assert!(re.matches_ffi(&L!("abcd").to_ffi()));
assert!(!re.matches_ffi(&L!("xabcd").to_ffi()));
assert!(re.matches_ffi(&L!("abcdefghij").to_ffi()));
let mut result = ffi::try_compile(&regex_make_anchored(L!("(a+)|(b+)")), &flags);
assert!(!result.has_error());
let re = result.as_mut().get_regex();
assert!(!re.is_null());
assert!(!re.matches_ffi(&L!("").to_ffi()));
assert!(!re.matches_ffi(&L!("aabb").to_ffi()));
assert!(re.matches_ffi(&L!("aaaa").to_ffi()));
assert!(re.matches_ffi(&L!("bbbb").to_ffi()));
assert!(!re.matches_ffi(&L!("aaaax").to_ffi()));
});

View File

@ -1,240 +0,0 @@
//! This file supports specifying and applying redirections.
use crate::ffi::wcharz_t;
use crate::wchar::{WString, L};
use crate::wchar_ffi::WCharToFFI;
use crate::wutil::fish_wcstoi;
use cxx::{CxxVector, CxxWString, SharedPtr, UniquePtr};
use libc::{c_int, O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_TRUNC, O_WRONLY};
use std::os::fd::RawFd;
#[cxx::bridge]
mod redirection_ffi {
extern "C++" {
include!("wutil.h");
type wcharz_t = super::wcharz_t;
}
enum RedirectionMode {
overwrite, // normal redirection: > file.txt
append, // appending redirection: >> file.txt
input, // input redirection: < file.txt
fd, // fd redirection: 2>&1
noclob, // noclobber redirection: >? file.txt
}
extern "Rust" {
type RedirectionSpec;
fn is_close(self: &RedirectionSpec) -> bool;
#[cxx_name = "get_target_as_fd"]
fn get_target_as_fd_ffi(self: &RedirectionSpec) -> SharedPtr<i32>;
fn oflags(self: &RedirectionSpec) -> i32;
fn fd(self: &RedirectionSpec) -> i32;
fn mode(self: &RedirectionSpec) -> RedirectionMode;
fn target(self: &RedirectionSpec) -> UniquePtr<CxxWString>;
fn new_redirection_spec(
fd: i32,
mode: RedirectionMode,
target: wcharz_t,
) -> Box<RedirectionSpec>;
type RedirectionSpecList;
fn new_redirection_spec_list() -> Box<RedirectionSpecList>;
fn size(self: &RedirectionSpecList) -> usize;
fn at(self: &RedirectionSpecList, offset: usize) -> *const RedirectionSpec;
fn push_back(self: &mut RedirectionSpecList, spec: Box<RedirectionSpec>);
fn clone(self: &RedirectionSpecList) -> Box<RedirectionSpecList>;
}
/// A type that represents the action dup2(src, target).
/// If target is negative, this represents close(src).
/// Note none of the fds here are considered 'owned'.
#[derive(Clone, Copy)]
struct Dup2Action {
src: i32,
target: i32,
}
/// A class representing a sequence of basic redirections.
struct Dup2List {
/// The list of actions.
actions: Vec<Dup2Action>,
}
extern "Rust" {
fn get_actions(self: &Dup2List) -> &Vec<Dup2Action>;
#[cxx_name = "dup2_list_resolve_chain"]
fn dup2_list_resolve_chain_ffi(io_chain: &CxxVector<Dup2Action>) -> Dup2List;
fn fd_for_target_fd(self: &Dup2List, target: i32) -> i32;
}
}
pub use redirection_ffi::{Dup2Action, Dup2List, RedirectionMode};
impl RedirectionMode {
/// The open flags for this redirection mode.
pub fn oflags(self) -> Option<c_int> {
match self {
RedirectionMode::append => Some(O_CREAT | O_APPEND | O_WRONLY),
RedirectionMode::overwrite => Some(O_CREAT | O_WRONLY | O_TRUNC),
RedirectionMode::noclob => Some(O_CREAT | O_EXCL | O_WRONLY),
RedirectionMode::input => Some(O_RDONLY),
_ => None,
}
}
}
/// A struct which represents a redirection specification from the user.
/// Here the file descriptors don't represent open files - it's purely textual.
#[derive(Clone)]
pub struct RedirectionSpec {
/// The redirected fd, or -1 on overflow.
/// In the common case of a pipe, this is 1 (STDOUT_FILENO).
/// For example, in the case of "3>&1" this will be 3.
fd: RawFd,
/// The redirection mode.
mode: RedirectionMode,
/// The target of the redirection.
/// For example in "3>&1", this will be "1".
/// In "< file.txt" this will be "file.txt".
target: WString,
}
impl RedirectionSpec {
/// \return if this is a close-type redirection.
pub fn is_close(&self) -> bool {
self.mode == RedirectionMode::fd && self.target == L!("-")
}
/// Attempt to parse target as an fd.
pub fn get_target_as_fd(&self) -> Option<RawFd> {
fish_wcstoi(self.target.as_char_slice().iter().copied()).ok()
}
fn get_target_as_fd_ffi(&self) -> SharedPtr<i32> {
match self.get_target_as_fd() {
Some(fd) => SharedPtr::new(fd),
None => SharedPtr::null(),
}
}
/// \return the open flags for this redirection.
pub fn oflags(&self) -> c_int {
match self.mode.oflags() {
Some(flags) => flags,
None => panic!("Not a file redirection"),
}
}
fn fd(&self) -> RawFd {
self.fd
}
fn mode(&self) -> RedirectionMode {
self.mode
}
fn target(&self) -> UniquePtr<CxxWString> {
self.target.to_ffi()
}
}
fn new_redirection_spec(fd: i32, mode: RedirectionMode, target: wcharz_t) -> Box<RedirectionSpec> {
Box::new(RedirectionSpec {
fd,
mode,
target: target.into(),
})
}
/// TODO This should be type alias once we drop the FFI.
pub struct RedirectionSpecList(Vec<RedirectionSpec>);
fn new_redirection_spec_list() -> Box<RedirectionSpecList> {
Box::new(RedirectionSpecList(Vec::new()))
}
impl RedirectionSpecList {
fn size(&self) -> usize {
self.0.len()
}
fn at(&self, offset: usize) -> *const RedirectionSpec {
&self.0[offset]
}
#[allow(clippy::boxed_local)]
fn push_back(self: &mut RedirectionSpecList, spec: Box<RedirectionSpec>) {
self.0.push(*spec)
}
fn clone(self: &RedirectionSpecList) -> Box<RedirectionSpecList> {
Box::new(RedirectionSpecList(self.0.clone()))
}
}
/// Produce a dup_fd_list_t from an io_chain. This may not be called before fork().
/// The result contains the list of fd actions (dup2 and close), as well as the list
/// of fds opened.
fn dup2_list_resolve_chain(io_chain: &Vec<Dup2Action>) -> Dup2List {
let mut result = Dup2List { actions: vec![] };
for io in io_chain {
if io.src < 0 {
result.add_close(io.target)
} else {
result.add_dup2(io.src, io.target)
}
}
result
}
fn dup2_list_resolve_chain_ffi(io_chain: &CxxVector<Dup2Action>) -> Dup2List {
dup2_list_resolve_chain(&io_chain.iter().cloned().collect())
}
impl Dup2List {
/// \return the list of dup2 actions.
fn get_actions(&self) -> &Vec<Dup2Action> {
&self.actions
}
/// \return the fd ultimately dup'd to a target fd, or -1 if the target is closed.
/// For example, if target fd is 1, and we have a dup2 chain 5->3 and 3->1, then we will
/// return 5. If the target is not referenced in the chain, returns target.
fn fd_for_target_fd(&self, target: RawFd) -> RawFd {
// Paranoia.
if target < 0 {
return target;
}
// Note we can simply walk our action list backwards, looking for src -> target dups.
let mut cursor = target;
for action in self.actions.iter().rev() {
if action.target == cursor {
// cursor is replaced by action.src
cursor = action.src;
} else if action.src == cursor && action.target < 0 {
// cursor is closed.
cursor = -1;
break;
}
}
cursor
}
/// Append a dup2 action.
fn add_dup2(&mut self, src: RawFd, target: RawFd) {
assert!(src >= 0 && target >= 0, "Invalid fd in add_dup2");
// Note: record these even if src and target is the same.
// This is a note that we must clear the CLO_EXEC bit.
self.actions.push(Dup2Action { src, target });
}
/// Append a close action.
fn add_close(&mut self, fd: RawFd) {
assert!(fd >= 0, "Invalid fd in add_close");
self.actions.push(Dup2Action {
src: fd,
target: -1,
})
}
}

View File

@ -1,67 +0,0 @@
use crate::ffi;
use crate::topic_monitor::{generation_t, invalid_generations, topic_monitor_principal, topic_t};
use crate::wchar::wstr;
use crate::wchar_ffi::c_str;
use widestring::U32CStr;
/// A sigint_detector_t can be used to check if a SIGINT (or SIGHUP) has been delivered.
pub struct sigchecker_t {
topic: topic_t,
gen: generation_t,
}
impl sigchecker_t {
/// Create a new checker for the given topic.
pub fn new(topic: topic_t) -> sigchecker_t {
let mut res = sigchecker_t { topic, gen: 0 };
// Call check() to update our generation.
res.check();
res
}
/// Create a new checker for SIGHUP and SIGINT.
pub fn new_sighupint() -> sigchecker_t {
Self::new(topic_t::sighupint)
}
/// Check if a sigint has been delivered since the last call to check(), or since the detector
/// was created.
pub fn check(&mut self) -> bool {
let tm = topic_monitor_principal();
let gen = tm.generation_for_topic(self.topic);
let changed = self.gen != gen;
self.gen = gen;
changed
}
/// Wait until a sigint is delivered.
pub fn wait(&self) {
let tm = topic_monitor_principal();
let mut gens = invalid_generations();
*gens.at_mut(self.topic) = self.gen;
tm.check(&mut gens, true /* wait */);
}
}
/// Get the integer signal value representing the specified signal.
pub fn wcs2sig(s: &wstr) -> Option<usize> {
let sig = ffi::wcs2sig(c_str!(s));
sig.0.try_into().ok()
}
/// Get string representation of a signal.
pub fn sig2wcs(sig: i32) -> &'static wstr {
let s = ffi::sig2wcs(ffi::c_int(sig));
let s = unsafe { U32CStr::from_ptr_str(s) };
wstr::from_ucstr(s).expect("signal name should be valid utf-32")
}
/// Returns a description of the specified signal.
pub fn signal_get_desc(sig: i32) -> &'static wstr {
let s = ffi::signal_get_desc(ffi::c_int(sig));
let s = unsafe { U32CStr::from_ptr_str(s) };
wstr::from_ucstr(s).expect("signal description should be valid utf-32")
}

View File

@ -1,26 +0,0 @@
#[cxx::bridge(namespace = rust)]
mod ffi {
extern "Rust" {
fn add(left: usize, right: usize) -> usize;
}
}
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
use crate::ffi_tests::add_test;
add_test!("test_add", || {
assert_eq!(add(2, 3), 5);
});

View File

@ -1,190 +0,0 @@
use std::io::Write;
use std::os::fd::AsRawFd;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::fd_monitor::{FdMonitor, FdMonitorItem, FdMonitorItemId, ItemWakeReason};
use crate::fds::{make_autoclose_pipes, AutoCloseFd};
use crate::ffi_tests::add_test;
/// Helper to make an item which counts how many times its callback was invoked.
///
/// This could be structured differently to avoid the `Mutex` on `writer`, but it's not worth it
/// since this is just used for test purposes.
struct ItemMaker {
pub did_timeout: AtomicBool,
pub length_read: AtomicUsize,
pub pokes: AtomicUsize,
pub total_calls: AtomicUsize,
item_id: AtomicU64,
pub always_exit: bool,
pub writer: Mutex<AutoCloseFd>,
}
impl ItemMaker {
pub fn insert_new_into(monitor: &FdMonitor, timeout: Option<Duration>) -> Arc<Self> {
Self::insert_new_into2(monitor, timeout, |_| {})
}
pub fn insert_new_into2<F: Fn(&mut Self)>(
monitor: &FdMonitor,
timeout: Option<Duration>,
config: F,
) -> Arc<Self> {
let pipes = make_autoclose_pipes().expect("fds exhausted!");
let mut item = FdMonitorItem::new(pipes.read, timeout, None);
let mut result = ItemMaker {
did_timeout: false.into(),
length_read: 0.into(),
pokes: 0.into(),
total_calls: 0.into(),
item_id: 0.into(),
always_exit: false,
writer: Mutex::new(pipes.write),
};
config(&mut result);
let result = Arc::new(result);
let callback = {
let result = Arc::clone(&result);
move |fd: &mut AutoCloseFd, reason: ItemWakeReason| {
result.callback(fd, reason);
}
};
item.set_callback(Box::new(callback));
let item_id = monitor.add(item);
result.item_id.store(u64::from(item_id), Ordering::Relaxed);
result
}
fn item_id(&self) -> FdMonitorItemId {
self.item_id.load(Ordering::Relaxed).into()
}
fn callback(&self, fd: &mut AutoCloseFd, reason: ItemWakeReason) {
let mut was_closed = false;
match reason {
ItemWakeReason::Timeout => {
self.did_timeout.store(true, Ordering::Relaxed);
}
ItemWakeReason::Poke => {
self.pokes.fetch_add(1, Ordering::Relaxed);
}
ItemWakeReason::Readable => {
let mut buf = [0u8; 1024];
let amt =
unsafe { libc::read(fd.as_raw_fd(), buf.as_mut_ptr() as *mut _, buf.len()) };
assert_ne!(amt, -1, "read error!");
self.length_read.fetch_add(amt as usize, Ordering::Relaxed);
was_closed = amt == 0;
}
_ => unreachable!(),
}
self.total_calls.fetch_add(1, Ordering::Relaxed);
if self.always_exit || was_closed {
fd.close();
}
}
/// Write 42 bytes to our write end.
fn write42(&self) {
let buf = [0u8; 42];
let mut writer = self.writer.lock().expect("Mutex poisoned!");
writer
.write_all(&buf)
.expect("Error writing 42 bytes to pipe!");
}
}
add_test!("fd_monitor_items", || {
let monitor = FdMonitor::new();
// Items which will never receive data or be called.
let item_never = ItemMaker::insert_new_into(&monitor, None);
let item_huge_timeout =
ItemMaker::insert_new_into(&monitor, Some(Duration::from_millis(100_000_000)));
// Item which should get no data and time out.
let item0_timeout = ItemMaker::insert_new_into(&monitor, Some(Duration::from_millis(16)));
// Item which should get exactly 42 bytes then time out.
let item42_timeout = ItemMaker::insert_new_into(&monitor, Some(Duration::from_millis(16)));
// Item which should get exactly 42 bytes and not time out.
let item42_no_timeout = ItemMaker::insert_new_into(&monitor, None);
// Item which should get 42 bytes then get notified it is closed.
let item42_then_close = ItemMaker::insert_new_into(&monitor, Some(Duration::from_millis(16)));
// Item which gets one poke.
let item_pokee = ItemMaker::insert_new_into(&monitor, None);
// Item which should get a callback exactly once.
let item_oneshot =
ItemMaker::insert_new_into2(&monitor, Some(Duration::from_millis(16)), |item| {
item.always_exit = true;
});
item42_timeout.write42();
item42_no_timeout.write42();
item42_then_close.write42();
item42_then_close
.writer
.lock()
.expect("Mutex poisoned!")
.close();
item_oneshot.write42();
monitor.poke_item(item_pokee.item_id());
// May need to loop here to ensure our fd_monitor gets scheduled. See #7699.
for _ in 0..100 {
std::thread::sleep(Duration::from_millis(84));
if item0_timeout.did_timeout.load(Ordering::Relaxed) {
break;
}
}
drop(monitor);
assert_eq!(item_never.did_timeout.load(Ordering::Relaxed), false);
assert_eq!(item_never.length_read.load(Ordering::Relaxed), 0);
assert_eq!(item_never.pokes.load(Ordering::Relaxed), 0);
assert_eq!(item_huge_timeout.did_timeout.load(Ordering::Relaxed), false);
assert_eq!(item_huge_timeout.length_read.load(Ordering::Relaxed), 0);
assert_eq!(item_huge_timeout.pokes.load(Ordering::Relaxed), 0);
assert_eq!(item0_timeout.length_read.load(Ordering::Relaxed), 0);
assert_eq!(item0_timeout.did_timeout.load(Ordering::Relaxed), true);
assert_eq!(item0_timeout.pokes.load(Ordering::Relaxed), 0);
assert_eq!(item42_timeout.length_read.load(Ordering::Relaxed), 42);
assert_eq!(item42_timeout.did_timeout.load(Ordering::Relaxed), true);
assert_eq!(item42_timeout.pokes.load(Ordering::Relaxed), 0);
assert_eq!(item42_no_timeout.length_read.load(Ordering::Relaxed), 42);
assert_eq!(item42_no_timeout.did_timeout.load(Ordering::Relaxed), false);
assert_eq!(item42_no_timeout.pokes.load(Ordering::Relaxed), 0);
assert_eq!(item42_then_close.did_timeout.load(Ordering::Relaxed), false);
assert_eq!(item42_then_close.length_read.load(Ordering::Relaxed), 42);
assert_eq!(item42_then_close.total_calls.load(Ordering::Relaxed), 2);
assert_eq!(item42_then_close.pokes.load(Ordering::Relaxed), 0);
assert_eq!(item_oneshot.did_timeout.load(Ordering::Relaxed), false);
assert_eq!(item_oneshot.length_read.load(Ordering::Relaxed), 42);
assert_eq!(item_oneshot.total_calls.load(Ordering::Relaxed), 1);
assert_eq!(item_oneshot.pokes.load(Ordering::Relaxed), 0);
assert_eq!(item_pokee.did_timeout.load(Ordering::Relaxed), false);
assert_eq!(item_pokee.length_read.load(Ordering::Relaxed), 0);
assert_eq!(item_pokee.total_calls.load(Ordering::Relaxed), 1);
assert_eq!(item_pokee.pokes.load(Ordering::Relaxed), 1);
});

View File

@ -1 +0,0 @@
mod fd_monitor;

View File

@ -1,193 +0,0 @@
//! The rusty version of iothreads from the cpp code, to be consumed by native rust code. This isn't
//! ported directly from the cpp code so we can use rust threads instead of using pthreads.
use crate::flog::{FloggableDebug, FLOG};
use std::thread::{self, ThreadId};
impl FloggableDebug for ThreadId {}
// We don't want to use a full-blown Lazy<T> for the cached main thread id, but we can't use
// AtomicU64 since std::thread::ThreadId::as_u64() is a nightly-only feature (issue #67939,
// thread_id_value). We also can't safely transmute `ThreadId` to `NonZeroU64` because there's no
// guarantee that's what the underlying type will always be on all platforms and in all cases,
// `ThreadId` isn't marked `#[repr(transparent)]`. We could generate our own thread-local value, but
// `#[thread_local]` is nightly-only while the stable `thread_local!()` macro doesn't generate
// efficient/fast/low-overhead code.
/// The thread id of the main thread, as set by [`init()`] at startup.
static mut MAIN_THREAD_ID: Option<ThreadId> = None;
/// Initialize some global static variables. Must be called at startup from the main thread.
pub fn init() {
unsafe {
if MAIN_THREAD_ID.is_some() {
panic!("threads::init() must only be called once (at startup)!");
}
MAIN_THREAD_ID = Some(thread::current().id());
}
}
#[inline(always)]
fn main_thread_id() -> ThreadId {
#[cold]
fn init_not_called() -> ! {
panic!("threads::init() was not called at startup!");
}
match unsafe { MAIN_THREAD_ID } {
None => init_not_called(),
Some(id) => id,
}
}
#[inline(always)]
pub fn assert_is_main_thread() {
#[cold]
fn not_main_thread() -> ! {
panic!("Function is not running on the main thread!");
}
if thread::current().id() != main_thread_id() {
not_main_thread();
}
}
#[inline(always)]
pub fn assert_is_background_thread() {
#[cold]
fn not_background_thread() -> ! {
panic!("Function is not allowed to be called on the main thread!");
}
if thread::current().id() == main_thread_id() {
not_background_thread();
}
}
/// The rusty version of `iothreads::make_detached_pthread()`. We will probably need a
/// `spawn_scoped` version of the same to handle some more advanced borrow cases safely, and maybe
/// an unsafe version that doesn't do any lifetime checking akin to
/// `spawn_unchecked()`[std::thread::Builder::spawn_unchecked], which is a nightly-only feature.
///
/// Returns a boolean indicating whether or not the thread was successfully launched. Failure here
/// is not dependent on the passed callback and implies a system error (likely insufficient
/// resources).
pub fn spawn<F: FnOnce() + Send + 'static>(callback: F) -> bool {
// The spawned thread inherits our signal mask. Temporarily block signals, spawn the thread, and
// then restore it. But we must not block SIGBUS, SIGFPE, SIGILL, or SIGSEGV; that's undefined
// (#7837). Conservatively don't try to mask SIGKILL or SIGSTOP either; that's ignored on Linux
// but maybe has an effect elsewhere.
let saved_set = unsafe {
let mut new_set: libc::sigset_t = std::mem::zeroed();
let new_set = &mut new_set as *mut _;
libc::sigfillset(new_set);
libc::sigdelset(new_set, libc::SIGILL); // bad jump
libc::sigdelset(new_set, libc::SIGFPE); // divide-by-zero
libc::sigdelset(new_set, libc::SIGBUS); // unaligned memory access
libc::sigdelset(new_set, libc::SIGSEGV); // bad memory access
libc::sigdelset(new_set, libc::SIGSTOP); // unblockable
libc::sigdelset(new_set, libc::SIGKILL); // unblockable
let mut saved_set: libc::sigset_t = std::mem::zeroed();
let result = libc::pthread_sigmask(libc::SIG_BLOCK, new_set, &mut saved_set as *mut _);
assert_eq!(result, 0, "Failed to override thread signal mask!");
saved_set
};
// Spawn a thread. If this fails, it means there's already a bunch of threads; it is very
// unlikely that they are all on the verge of exiting, so one is likely to be ready to handle
// extant requests. So we can ignore failure with some confidence.
// We don't have to port the PTHREAD_CREATE_DETACHED logic. Rust threads are detached
// automatically if the returned join handle is dropped.
let result = match std::thread::Builder::new().spawn(callback) {
Ok(handle) => {
let id = handle.thread().id();
FLOG!(iothread, "rust thread", id, "spawned");
// Drop the handle to detach the thread
drop(handle);
true
}
Err(e) => {
eprintln!("rust thread spawn failure: {e}");
false
}
};
// Restore our sigmask
unsafe {
let result = libc::pthread_sigmask(
libc::SIG_SETMASK,
&saved_set as *const _,
std::ptr::null_mut(),
);
assert_eq!(result, 0, "Failed to restore thread signal mask!");
};
result
}
#[test]
/// Verify that spawing a thread normally via [`std::thread::spawn()`] causes the calling thread's
/// sigmask to be inherited by the newly spawned thread.
fn std_thread_inherits_sigmask() {
// First change our own thread mask
let (saved_set, t1_set) = unsafe {
let mut new_set: libc::sigset_t = std::mem::zeroed();
let new_set = &mut new_set as *mut _;
libc::sigemptyset(new_set);
libc::sigaddset(new_set, libc::SIGILL); // mask bad jump
let mut saved_set: libc::sigset_t = std::mem::zeroed();
let result = libc::pthread_sigmask(libc::SIG_BLOCK, new_set, &mut saved_set as *mut _);
assert_eq!(result, 0, "Failed to set thread mask!");
// Now get the current set that includes the masked SIGILL
let mut t1_set: libc::sigset_t = std::mem::zeroed();
let mut empty_set = std::mem::zeroed();
let empty_set = &mut empty_set as *mut _;
libc::sigemptyset(empty_set);
let result = libc::pthread_sigmask(libc::SIG_UNBLOCK, empty_set, &mut t1_set as *mut _);
assert_eq!(result, 0, "Failed to get own altered thread mask!");
(saved_set, t1_set)
};
// Launch a new thread that can access existing variables
let t2_set = std::thread::scope(|_| {
unsafe {
// Set a new thread sigmask and verify that the old one is what we expect it to be
let mut new_set: libc::sigset_t = std::mem::zeroed();
let new_set = &mut new_set as *mut _;
libc::sigemptyset(new_set);
let mut saved_set2: libc::sigset_t = std::mem::zeroed();
let result = libc::pthread_sigmask(libc::SIG_BLOCK, new_set, &mut saved_set2 as *mut _);
assert_eq!(result, 0, "Failed to get existing sigmask for new thread");
saved_set2
}
});
// Compare the sigset_t values
unsafe {
let t1_sigset_slice = std::slice::from_raw_parts(
&t1_set as *const _ as *const u8,
core::mem::size_of::<libc::sigset_t>(),
);
let t2_sigset_slice = std::slice::from_raw_parts(
&t2_set as *const _ as *const u8,
core::mem::size_of::<libc::sigset_t>(),
);
assert_eq!(t1_sigset_slice, t2_sigset_slice);
};
// Restore the thread sigset so we don't affect `cargo test`'s multithreaded test harnesses
unsafe {
let result = libc::pthread_sigmask(
libc::SIG_SETMASK,
&saved_set as *const _,
core::ptr::null_mut(),
);
assert_eq!(result, 0, "Failed to restore sigmask!");
}
}

View File

@ -1,265 +0,0 @@
//! This module houses `TimerSnapshot` which can be used to calculate the elapsed time (system CPU
//! time, user CPU time, and observed wall time, broken down by fish and child processes spawned by
//! fish) between two `TimerSnapshot` instances.
//!
//! Measuring time is always complicated with many caveats. Quite apart from the typical
//! gotchas faced by developers attempting to choose between monotonic vs non-monotonic and system vs
//! cpu clocks, the fact that we are executing as a shell further complicates matters: we can't just
//! observe the elapsed CPU time, because that does not reflect the total execution time for both
//! ourselves (internal shell execution time and the time it takes for builtins and functions to
//! execute) and any external processes we spawn.
//!
//! `std::time::Instant` is used to monitor elapsed wall time. Unlike `SystemTime`, `Instant` is
//! guaranteed to be monotonic though it is likely to not be as high of a precision as we would like
//! but it's still the best we can do because we don't know how long of a time might elapse between
//! `TimerSnapshot` instances and need to avoid rollover.
use std::io::Write;
use std::time::{Duration, Instant};
#[cxx::bridge]
mod timer_ffi {
extern "Rust" {
type PrintElapsedOnDropFfi;
#[cxx_name = "push_timer"]
fn push_timer_ffi(enabled: bool) -> Box<PrintElapsedOnDropFfi>;
}
}
enum Unit {
Minutes,
Seconds,
Millis,
Micros,
}
struct TimerSnapshot {
wall_time: Instant,
cpu_fish: libc::rusage,
cpu_children: libc::rusage,
}
/// If `enabled`, create a `TimerSnapshot` and return a `PrintElapsedOnDrop` object that will print
/// upon being dropped the delta between now and the time that it is dropped at. Otherwise return
/// `None`.
pub fn push_timer(enabled: bool) -> Option<PrintElapsedOnDrop> {
if !enabled {
return None;
}
Some(PrintElapsedOnDrop {
start: TimerSnapshot::take(),
})
}
/// cxx bridge does not support UniquePtr<NativeRustType> so we can't use a null UniquePtr to
/// represent a None, and cxx bridge does not support Box<Option<NativeRustType>> so we need to make
/// our own wrapper type that incorporates the Some/None states directly into it.
#[allow(clippy::large_enum_variant)]
enum PrintElapsedOnDropFfi {
Some(PrintElapsedOnDrop),
None,
}
fn push_timer_ffi(enabled: bool) -> Box<PrintElapsedOnDropFfi> {
Box::new(match push_timer(enabled) {
Some(t) => PrintElapsedOnDropFfi::Some(t),
None => PrintElapsedOnDropFfi::None,
})
}
/// An enumeration of supported libc rusage types used by [`getrusage()`].
/// NB: RUSAGE_THREAD is not supported on macOS.
enum RUsage {
RSelf, // "Self" is a reserved keyword
RChildren,
}
/// A safe wrapper around `libc::getrusage()`
fn getrusage(resource: RUsage) -> libc::rusage {
let mut rusage = std::mem::MaybeUninit::uninit();
let result = unsafe {
match resource {
RUsage::RSelf => libc::getrusage(libc::RUSAGE_SELF, rusage.as_mut_ptr()),
RUsage::RChildren => libc::getrusage(libc::RUSAGE_CHILDREN, rusage.as_mut_ptr()),
}
};
// getrusage(2) says the syscall can only fail if the dest address is invalid (EFAULT) or if the
// requested resource type is invalid. Since we're in control of both, we can assume it won't
// fail. In case it does anyway (e.g. OS where the syscall isn't implemented), we can just
// return an empty value.
match result {
0 => unsafe { rusage.assume_init() },
_ => unsafe { std::mem::zeroed() },
}
}
impl TimerSnapshot {
pub fn take() -> TimerSnapshot {
TimerSnapshot {
cpu_fish: getrusage(RUsage::RSelf),
cpu_children: getrusage(RUsage::RChildren),
wall_time: Instant::now(),
}
}
/// Returns a formatted string containing the detailed difference between two `TimerSnapshot`
/// instances. The returned string can take one of two formats, depending on the value of the
/// `verbose` parameter.
pub fn get_delta(t1: &TimerSnapshot, t2: &TimerSnapshot, verbose: bool) -> String {
use crate::nix::timeval_to_duration as from;
let mut fish_sys = from(&t2.cpu_fish.ru_stime) - from(&t1.cpu_fish.ru_stime);
let mut fish_usr = from(&t2.cpu_fish.ru_utime) - from(&t1.cpu_fish.ru_utime);
let mut child_sys = from(&t2.cpu_children.ru_stime) - from(&t1.cpu_children.ru_stime);
let mut child_usr = from(&t2.cpu_children.ru_utime) - from(&t1.cpu_children.ru_utime);
// The result from getrusage is not necessarily realtime, it may be cached from a few
// microseconds ago. In the event that execution completes extremely quickly or there is
// no data (say, we are measuring external execution time but no external processes have
// been launched), it can incorrectly appear to be negative.
fish_sys = fish_sys.max(Duration::ZERO);
fish_usr = fish_usr.max(Duration::ZERO);
child_sys = child_sys.max(Duration::ZERO);
child_usr = child_usr.max(Duration::ZERO);
// As `Instant` is strictly monotonic, this can't be negative so we don't need to clamp.
let net_wall_micros = (t2.wall_time - t1.wall_time).as_micros() as i64;
let net_sys_micros = (fish_sys + child_sys).as_micros() as i64;
let net_usr_micros = (fish_usr + child_usr).as_micros() as i64;
let wall_unit = Unit::for_micros(net_wall_micros);
// Make sure we share the same unit for the various CPU times
let cpu_unit = Unit::for_micros(net_sys_micros.max(net_usr_micros));
let wall_time = wall_unit.convert_micros(net_wall_micros);
let sys_time = cpu_unit.convert_micros(net_sys_micros);
let usr_time = cpu_unit.convert_micros(net_usr_micros);
let mut output = String::new();
if !verbose {
output += "\n_______________________________";
output += &format!("\nExecuted in {:6.2} {}", wall_time, wall_unit.long_name());
output += &format!("\n usr time {:6.2} {}", usr_time, cpu_unit.long_name());
output += &format!("\n sys time {:6.2} {}", sys_time, cpu_unit.long_name());
} else {
let fish_unit = Unit::for_micros(fish_sys.max(fish_usr).as_micros() as i64);
let child_unit = Unit::for_micros(child_sys.max(child_usr).as_micros() as i64);
let fish_usr_time = fish_unit.convert_micros(fish_usr.as_micros() as i64);
let fish_sys_time = fish_unit.convert_micros(fish_sys.as_micros() as i64);
let child_usr_time = child_unit.convert_micros(child_usr.as_micros() as i64);
let child_sys_time = child_unit.convert_micros(child_sys.as_micros() as i64);
let column2_unit_len = wall_unit
.short_name()
.len()
.max(cpu_unit.short_name().len());
let wall_unit = wall_unit.short_name();
let cpu_unit = cpu_unit.short_name();
let fish_unit = fish_unit.short_name();
let child_unit = child_unit.short_name();
output += "\n________________________________________________________";
output += &format!(
"\nExecuted in {wall_time:6.2} {wall_unit:<width1$} {fish:<width2$} external",
width1 = column2_unit_len,
fish = "fish",
width2 = fish_unit.len() + 7
);
output += &format!("\n usr time {usr_time:6.2} {cpu_unit:<column2_unit_len$} {fish_usr_time:6.2} {fish_unit} {child_usr_time:6.2} {child_unit}");
output += &format!("\n sys time {sys_time:6.2} {cpu_unit:<column2_unit_len$} {fish_sys_time:6.2} {fish_unit} {child_sys_time:6.2} {child_unit}");
}
output += "\n";
output
}
}
/// When dropped, prints to stderr the time that has elapsed since it was initialized.
pub struct PrintElapsedOnDrop {
start: TimerSnapshot,
}
impl Drop for PrintElapsedOnDrop {
fn drop(&mut self) {
let end = TimerSnapshot::take();
// Well, this is awkward. By defining `time` as a decorator and not a built-in, there's
// no associated stream for its output!
let output = TimerSnapshot::get_delta(&self.start, &end, true);
let mut stderr = std::io::stderr().lock();
// There is no bubbling up of errors in a Drop implementation, and it's absolutely forbidden
// to panic.
let _ = stderr.write_all(output.as_bytes());
let _ = stderr.write_all(b"\n");
}
}
impl Unit {
/// Return the appropriate unit to format the provided number of microseconds in.
const fn for_micros(micros: i64) -> Unit {
match micros {
900_000_001.. => Unit::Minutes,
// Move to seconds if we would overflow the %6.2 format
999_995.. => Unit::Seconds,
1000.. => Unit::Millis,
_ => Unit::Micros,
}
}
const fn short_name(&self) -> &'static str {
match *self {
Unit::Minutes => "mins",
Unit::Seconds => "secs",
Unit::Millis => "millis",
Unit::Micros => "micros",
}
}
const fn long_name(&self) -> &'static str {
match *self {
Unit::Minutes => "minutes",
Unit::Seconds => "seconds",
Unit::Millis => "milliseconds",
Unit::Micros => "microseconds",
}
}
fn convert_micros(&self, micros: i64) -> f64 {
match *self {
Unit::Minutes => micros as f64 / 1.0E6 / 60.0,
Unit::Seconds => micros as f64 / 1.0E6,
Unit::Millis => micros as f64 / 1.0E3,
Unit::Micros => micros as f64 / 1.0,
}
}
}
#[test]
fn timer_format_and_alignment() {
let mut t1 = TimerSnapshot::take();
t1.cpu_fish.ru_utime.tv_usec = 0;
t1.cpu_fish.ru_stime.tv_usec = 0;
t1.cpu_children.ru_utime.tv_usec = 0;
t1.cpu_children.ru_stime.tv_usec = 0;
let mut t2 = TimerSnapshot::take();
t2.cpu_fish.ru_utime.tv_usec = 999995;
t2.cpu_fish.ru_stime.tv_usec = 999994;
t2.cpu_children.ru_utime.tv_usec = 1000;
t2.cpu_children.ru_stime.tv_usec = 500;
t2.wall_time = t1.wall_time + Duration::from_micros(500);
let expected = r#"
________________________________________________________
Executed in 500.00 micros fish external
usr time 1.00 secs 1.00 secs 1.00 millis
sys time 1.00 secs 1.00 secs 0.50 millis
"#;
// (a) (b) (c)
// (a) remaining columns should align even if there are different units
// (b) carry to the next unit when it would overflow %6.2F
// (c) carry to the next unit when the larger one exceeds 1000
let actual = TimerSnapshot::get_delta(&t1, &t2, true);
assert_eq!(actual, expected);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,644 +0,0 @@
/*! Topic monitoring support.
Topics are conceptually "a thing that can happen." For example,
delivery of a SIGINT, a child process exits, etc. It is possible to post to a topic, which means
that that thing happened.
Associated with each topic is a current generation, which is a 64 bit value. When you query a
topic, you get back a generation. If on the next query the generation has increased, then it
indicates someone posted to the topic.
For example, if you are monitoring a child process, you can query the sigchld topic. If it has
increased since your last query, it is possible that your child process has exited.
Topic postings may be coalesced. That is there may be two posts to a given topic, yet the
generation only increases by 1. The only guarantee is that after a topic post, the current
generation value is larger than any value previously queried.
Tying this all together is the topic_monitor_t. This provides the current topic generations, and
also provides the ability to perform a blocking wait for any topic to change in a particular topic
set. This is the real power of topics: you can wait for a sigchld signal OR a thread exit.
*/
use crate::fd_readable_set::fd_readable_set_t;
use crate::fds::{self, autoclose_pipes_t};
use crate::ffi::{self as ffi, c_int};
use crate::flog::{FloggableDebug, FLOG};
use crate::wchar::{widestrs, wstr, WString};
use crate::wchar_ffi::wcharz;
use nix::errno::Errno;
use nix::unistd;
use std::cell::UnsafeCell;
use std::mem;
use std::pin::Pin;
use std::sync::{
atomic::{AtomicU8, Ordering},
Condvar, Mutex, MutexGuard,
};
#[cxx::bridge]
mod topic_monitor_ffi {
/// Simple value type containing the values for a topic.
/// This should be kept in sync with topic_t.
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
struct generation_list_t {
pub sighupint: u64,
pub sigchld: u64,
pub internal_exit: u64,
}
extern "Rust" {
fn invalid_generations() -> generation_list_t;
fn set_min_from(self: &mut generation_list_t, topic: topic_t, other: &generation_list_t);
fn at(self: &generation_list_t, topic: topic_t) -> u64;
fn at_mut(self: &mut generation_list_t, topic: topic_t) -> &mut u64;
//fn describe(self: &generation_list_t) -> UniquePtr<wcstring>;
}
/// The list of topics which may be observed.
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum topic_t {
sighupint, // Corresponds to both SIGHUP and SIGINT signals.
sigchld, // Corresponds to SIGCHLD signal.
internal_exit, // Corresponds to an internal process exit.
}
extern "Rust" {
type topic_monitor_t;
fn new_topic_monitor() -> Box<topic_monitor_t>;
fn topic_monitor_principal() -> &'static topic_monitor_t;
fn post(self: &topic_monitor_t, topic: topic_t);
fn current_generations(self: &topic_monitor_t) -> generation_list_t;
fn generation_for_topic(self: &topic_monitor_t, topic: topic_t) -> u64;
fn check(self: &topic_monitor_t, gens: *mut generation_list_t, wait: bool) -> bool;
}
}
pub use topic_monitor_ffi::{generation_list_t, topic_t};
pub type generation_t = u64;
impl FloggableDebug for topic_t {}
/// A generation value which indicates the topic is not of interest.
pub const invalid_generation: generation_t = std::u64::MAX;
pub fn all_topics() -> [topic_t; 3] {
[topic_t::sighupint, topic_t::sigchld, topic_t::internal_exit]
}
#[widestrs]
impl generation_list_t {
pub fn new() -> Self {
Self::default()
}
fn describe(&self) -> WString {
let mut result = WString::new();
for gen in self.as_array() {
if result.len() > 0 {
result.push(',');
}
if gen == invalid_generation {
result.push_str("-1");
} else {
result.push_str(&gen.to_string());
}
}
return result;
}
/// \return the a mutable reference to the value for a topic.
pub fn at_mut(&mut self, topic: topic_t) -> &mut generation_t {
match topic {
topic_t::sighupint => &mut self.sighupint,
topic_t::sigchld => &mut self.sigchld,
topic_t::internal_exit => &mut self.internal_exit,
_ => panic!("invalid topic"),
}
}
/// \return the value for a topic.
pub fn at(&self, topic: topic_t) -> generation_t {
match topic {
topic_t::sighupint => self.sighupint,
topic_t::sigchld => self.sigchld,
topic_t::internal_exit => self.internal_exit,
_ => panic!("invalid topic"),
}
}
/// \return ourselves as an array.
pub fn as_array(&self) -> [generation_t; 3] {
[self.sighupint, self.sigchld, self.internal_exit]
}
/// Set the value of \p topic to the smaller of our value and the value in \p other.
pub fn set_min_from(&mut self, topic: topic_t, other: &generation_list_t) {
if self.at(topic) > other.at(topic) {
*self.at_mut(topic) = other.at(topic);
}
}
/// \return whether a topic is valid.
pub fn is_valid(&self, topic: topic_t) -> bool {
self.at(topic) != invalid_generation
}
/// \return whether any topic is valid.
pub fn any_valid(&self) -> bool {
let mut valid = false;
for gen in self.as_array() {
if gen != invalid_generation {
valid = true;
}
}
valid
}
/// Generation list containing invalid generations only.
pub fn invalids() -> generation_list_t {
generation_list_t {
sighupint: invalid_generation,
sigchld: invalid_generation,
internal_exit: invalid_generation,
}
}
}
/// CXX wrapper as it does not support member functions.
pub fn invalid_generations() -> generation_list_t {
generation_list_t::invalids()
}
/// A simple binary semaphore.
/// On systems that do not support unnamed semaphores (macOS in particular) this is built on top of
/// a self-pipe. Note that post() must be async-signal safe.
pub struct binary_semaphore_t {
// Whether our semaphore was successfully initialized.
sem_ok_: bool,
// The semaphore, if initalized.
// This is Box'd so it has a stable address.
sem_: Pin<Box<UnsafeCell<libc::sem_t>>>,
// Pipes used to emulate a semaphore, if not initialized.
pipes_: autoclose_pipes_t,
}
impl binary_semaphore_t {
pub fn new() -> binary_semaphore_t {
#[allow(unused_mut, unused_assignments)]
let mut sem_ok_ = false;
// sem_t does not have an initializer in Rust so we use zeroed().
#[allow(unused_mut)]
let mut sem_ = Pin::from(Box::new(UnsafeCell::new(unsafe { mem::zeroed() })));
let mut pipes_ = autoclose_pipes_t::default();
// sem_init always fails with ENOSYS on Mac and has an annoying deprecation warning.
// On BSD sem_init uses a file descriptor under the hood which doesn't get CLOEXEC (see #7304).
// So use fast semaphores on Linux only.
#[cfg(target_os = "linux")]
{
let res = unsafe { libc::sem_init(sem_.get(), 0, 0) };
sem_ok_ = res == 0;
}
if !sem_ok_ {
let pipes = fds::make_autoclose_pipes();
assert!(pipes.is_some(), "Failed to make pubsub pipes");
pipes_ = pipes.unwrap();
// // Whoof. Thread Sanitizer swallows signals and replays them at its leisure, at the point
// // where instrumented code makes certain blocking calls. But tsan cannot interrupt a signal
// // call, so if we're blocked in read() (like the topic monitor wants to be!), we'll never
// // receive SIGCHLD and so deadlock. So if tsan is enabled, we mark our fd as non-blocking
// // (so reads will never block) and use select() to poll it.
if cfg!(feature = "FISH_TSAN_WORKAROUNDS") {
ffi::make_fd_nonblocking(c_int(pipes_.read.fd()));
}
}
binary_semaphore_t {
sem_ok_,
sem_,
pipes_,
}
}
/// Release a waiting thread.
#[widestrs]
pub fn post(&self) {
// Beware, we are in a signal handler.
if self.sem_ok_ {
let res = unsafe { libc::sem_post(self.sem_.get()) };
// sem_post is non-interruptible.
if res < 0 {
self.die("sem_post"L);
}
} else {
// Write exactly one byte.
let success;
loop {
let v: u8 = 0;
let ret = unistd::write(self.pipes_.write.fd(), std::slice::from_ref(&v));
if ret.err() == Some(Errno::EINTR) {
continue;
}
success = ret.is_ok();
break;
}
if !success {
self.die("write"L);
}
}
}
/// Wait for a post.
/// This loops on EINTR.
#[widestrs]
pub fn wait(&self) {
if self.sem_ok_ {
let mut res;
loop {
res = unsafe { libc::sem_wait(self.sem_.get()) };
if res < 0 && Errno::last() == Errno::EINTR {
continue;
}
break;
}
// Other errors here are very unexpected.
if res < 0 {
self.die("sem_wait"L);
}
} else {
let fd = self.pipes_.read.fd();
// We must read exactly one byte.
loop {
// Under tsan our notifying pipe is non-blocking, so we would busy-loop on the read()
// call until data is available (that is, fish would use 100% cpu while waiting for
// processes). This call prevents that.
if cfg!(feature = "FISH_TSAN_WORKAROUNDS") {
let _ = fd_readable_set_t::is_fd_readable(fd, fd_readable_set_t::kNoTimeout);
}
let mut ignored: u8 = 0;
let amt = unistd::read(fd, std::slice::from_mut(&mut ignored));
if amt.ok() == Some(1) {
break;
}
// EAGAIN should only be returned in TSan case.
if amt.is_err()
&& (amt.err() != Some(Errno::EINTR) && amt.err() != Some(Errno::EAGAIN))
{
self.die("read"L);
}
}
}
}
pub fn die(&self, msg: &wstr) {
ffi::wperror(wcharz!(msg));
panic!("die");
}
}
impl Drop for binary_semaphore_t {
fn drop(&mut self) {
// We never use sem_t on Mac. The #ifdef avoids deprecation warnings.
#[cfg(target_os = "linux")]
{
if self.sem_ok_ {
_ = unsafe { libc::sem_destroy(self.sem_.get()) };
}
}
}
}
impl Default for binary_semaphore_t {
fn default() -> Self {
Self::new()
}
}
/// The topic monitor class. This permits querying the current generation values for topics,
/// optionally blocking until they increase.
/// What we would like to write is that we have a set of topics, and threads wait for changes on a
/// condition variable which is tickled in post(). But this can't work because post() may be called
/// from a signal handler and condition variables are not async-signal safe.
/// So instead the signal handler announces changes via a binary semaphore.
/// In the wait case, what generally happens is:
/// A thread fetches the generations, see they have not changed, and then decides to try to wait.
/// It does so by atomically swapping in STATUS_NEEDS_WAKEUP to the status bits.
/// If that succeeds, it waits on the binary semaphore. The post() call will then wake the thread
/// up. If if failed, then either a post() call updated the status values (so perhaps there is a
/// new topic post) or some other thread won the race and called wait() on the semaphore. Here our
/// thread will wait on the data_notifier_ queue.
type topic_bitmask_t = u8;
fn topic_to_bit(t: topic_t) -> topic_bitmask_t {
1 << t.repr
}
// Some stuff that needs to be protected by the same lock.
#[derive(Default)]
struct data_t {
/// The current values.
current: generation_list_t,
/// A flag indicating that there is a current reader.
/// The 'reader' is responsible for calling sema_.wait().
has_reader: bool,
}
/// Sentinel status value indicating that a thread is waiting and needs a wakeup.
/// Note it is an error for this bit to be set and also any topic bit.
const STATUS_NEEDS_WAKEUP: u8 = 128;
type status_bits_t = u8;
#[derive(Default)]
pub struct topic_monitor_t {
data_: Mutex<data_t>,
/// Condition variable for broadcasting notifications.
/// This is associated with data_'s mutex.
data_notifier_: Condvar,
/// A status value which describes our current state, managed via atomics.
/// Three possibilities:
/// 0: no changed topics, no thread is waiting.
/// 128: no changed topics, some thread is waiting and needs wakeup.
/// anything else: some changed topic, no thread is waiting.
/// Note that if the msb is set (status == 128) no other bit may be set.
status_: AtomicU8,
/// Binary semaphore used to communicate changes.
/// If status_ is STATUS_NEEDS_WAKEUP, then a thread has commited to call wait() on our sema and
/// this must be balanced by the next call to post(). Note only one thread may wait at a time.
sema_: binary_semaphore_t,
}
/// 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 topic_monitor_t = std::ptr::null();
/// Create a new topic monitor. Exposed for the FFI.
pub fn new_topic_monitor() -> Box<topic_monitor_t> {
Box::default()
}
impl topic_monitor_t {
/// 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() {
// We simply leak.
s_principal = Box::into_raw(new_topic_monitor());
}
&*s_principal
}
}
pub fn post(&self, topic: topic_t) {
// Beware, we may be in a signal handler!
// Atomically update the pending topics.
let topicbit = topic_to_bit(topic);
const relaxed: Ordering = Ordering::Relaxed;
// CAS in our bit, capturing the old status value.
let mut oldstatus: status_bits_t = 0;
let mut cas_success = false;
while !cas_success {
oldstatus = self.status_.load(relaxed);
// Clear wakeup bit and set our topic bit.
let mut newstatus = oldstatus;
newstatus &= !STATUS_NEEDS_WAKEUP; // note: bitwise not
newstatus |= topicbit;
cas_success = self
.status_
.compare_exchange_weak(oldstatus, newstatus, relaxed, relaxed)
.is_ok();
}
// Note that if the STATUS_NEEDS_WAKEUP bit is set, no other bits must be set.
assert!(
((oldstatus == STATUS_NEEDS_WAKEUP) == ((oldstatus & STATUS_NEEDS_WAKEUP) != 0)),
"If STATUS_NEEDS_WAKEUP is set no other bits should be set"
);
// If the bit was already set, then someone else posted to this topic and nobody has reacted to
// it yet. In that case we're done.
if (oldstatus & topicbit) != 0 {
return;
}
// We set a new bit.
// Check if we should wake up a thread because it was waiting.
if (oldstatus & STATUS_NEEDS_WAKEUP) != 0 {
std::sync::atomic::fence(Ordering::Release);
self.sema_.post();
}
}
/// Apply any pending updates to the data.
/// This accepts data because it must be locked.
/// \return the updated generation list.
fn updated_gens_in_data(&self, data: &mut MutexGuard<data_t>) -> generation_list_t {
// Atomically acquire the pending updates, swapping in 0.
// If there are no pending updates (likely) or a thread is waiting, just return.
// Otherwise CAS in 0 and update our topics.
const relaxed: Ordering = Ordering::Relaxed;
let mut changed_topic_bits: topic_bitmask_t = 0;
let mut cas_success = false;
while !cas_success {
changed_topic_bits = self.status_.load(relaxed);
if changed_topic_bits == 0 || changed_topic_bits == STATUS_NEEDS_WAKEUP {
return data.current;
}
cas_success = self
.status_
.compare_exchange_weak(changed_topic_bits, 0, relaxed, relaxed)
.is_ok();
}
assert!(
(changed_topic_bits & STATUS_NEEDS_WAKEUP) == 0,
"Thread waiting bit should not be set"
);
// Update the current generation with our topics and return it.
for topic in all_topics() {
if changed_topic_bits & topic_to_bit(topic) != 0 {
*data.current.at_mut(topic) += 1;
FLOG!(
topic_monitor,
"Updating topic",
topic,
"to",
data.current.at(topic)
);
}
}
// Report our change.
self.data_notifier_.notify_all();
return data.current;
}
/// \return the current generation list, opportunistically applying any pending updates.
fn updated_gens(&self) -> generation_list_t {
let mut data = self.data_.lock().unwrap();
return self.updated_gens_in_data(&mut data);
}
/// Access the current generations.
pub fn current_generations(self: &topic_monitor_t) -> generation_list_t {
self.updated_gens()
}
/// Access the generation for a topic.
pub fn generation_for_topic(self: &topic_monitor_t, topic: topic_t) -> generation_t {
self.current_generations().at(topic)
}
/// Given a list of input generations, attempt to update them to something newer.
/// If \p gens is older, then just return those by reference, and directly return false (not
/// becoming the reader).
/// If \p gens is current and there is not a reader, then do not update \p gens and return true,
/// indicating we should become the reader. Now it is our responsibility to wait on the
/// semaphore and notify on a change via the condition variable. If \p gens is current, and
/// there is already a reader, then wait until the reader notifies us and try again.
fn try_update_gens_maybe_becoming_reader(&self, gens: &mut generation_list_t) -> bool {
let mut become_reader = false;
let mut data = self.data_.lock().unwrap();
loop {
// See if the updated gen list has changed. If so we don't need to become the reader.
let current = self.updated_gens_in_data(&mut data);
// FLOG(topic_monitor, "TID", thread_id(), "local ", gens->describe(), ": current",
// current.describe());
if *gens != current {
*gens = current;
break;
}
// The generations haven't changed. Perhaps we become the reader.
// Note we still hold the lock, so this cannot race with any other thread becoming the
// reader.
if data.has_reader {
// We already have a reader, wait for it to notify us and loop again.
data = self.data_notifier_.wait(data).unwrap();
continue;
} else {
// We will try to become the reader.
// Reader bit should not be set in this case.
assert!(
(self.status_.load(Ordering::Relaxed) & STATUS_NEEDS_WAKEUP) == 0,
"No thread should be waiting"
);
// Try becoming the reader by marking the reader bit.
let expected_old: status_bits_t = 0;
if self
.status_
.compare_exchange(
expected_old,
STATUS_NEEDS_WAKEUP,
Ordering::SeqCst,
Ordering::SeqCst,
)
.is_err()
{
// We failed to become the reader, perhaps because another topic post just arrived.
// Loop again.
continue;
}
// We successfully did a CAS from 0 -> STATUS_NEEDS_WAKEUP.
// Now any successive topic post must signal us.
//FLOG(topic_monitor, "TID", thread_id(), "becoming reader");
become_reader = true;
data.has_reader = true;
break;
}
}
return become_reader;
}
/// Wait for some entry in the list of generations to change.
/// \return the new gens.
fn await_gens(&self, input_gens: &generation_list_t) -> generation_list_t {
let mut gens = *input_gens;
while gens == *input_gens {
let become_reader = self.try_update_gens_maybe_becoming_reader(&mut gens);
if become_reader {
// Now we are the reader. Read from the pipe, and then update with any changes.
// Note we no longer hold the lock.
assert!(
gens == *input_gens,
"Generations should not have changed if we are the reader."
);
// Wait to be woken up.
self.sema_.wait();
// We are finished waiting. We must stop being the reader, and post on the condition
// variable to wake up any other threads waiting for us to finish reading.
let mut data = self.data_.lock().unwrap();
gens = data.current;
// FLOG(topic_monitor, "TID", thread_id(), "local", input_gens.describe(),
// "read() complete, current is", gens.describe());
assert!(data.has_reader, "We should be the reader");
data.has_reader = false;
self.data_notifier_.notify_all();
}
}
return gens;
}
/// For each valid topic in \p gens, check to see if the current topic is larger than
/// the value in \p gens.
/// If \p wait is set, then wait if there are no changes; otherwise return immediately.
/// \return true if some topic changed, false if none did.
/// On a true return, this updates the generation list \p gens.
pub fn check(&self, gens: *mut generation_list_t, wait: bool) -> bool {
assert!(!gens.is_null(), "gens must not be null");
let gens = unsafe { &mut *gens };
if !gens.any_valid() {
return false;
}
let mut current: generation_list_t = self.updated_gens();
let mut changed = false;
loop {
// Load the topic list and see if anything has changed.
for topic in all_topics() {
if gens.is_valid(topic) {
assert!(
gens.at(topic) <= current.at(topic),
"Incoming gen count exceeded published count"
);
if gens.at(topic) < current.at(topic) {
*gens.at_mut(topic) = current.at(topic);
changed = true;
}
}
}
// If we're not waiting, or something changed, then we're done.
if !wait || changed {
break;
}
// Wait until our gens change.
current = self.await_gens(&current);
}
return changed;
}
}
pub fn topic_monitor_init() {
topic_monitor_t::initialize();
}
pub fn topic_monitor_principal() -> &'static topic_monitor_t {
unsafe {
assert!(
!s_principal.is_null(),
"Principal topic monitor not initialized"
);
&*s_principal
}
}

View File

@ -1,311 +0,0 @@
//! Generic utilities library.
use crate::ffi::wcharz_t;
use crate::wchar::wstr;
use std::cmp::Ordering;
use std::time;
#[cxx::bridge]
mod ffi {
extern "C++" {
include!("wutil.h");
type wcharz_t = super::wcharz_t;
}
extern "Rust" {
#[cxx_name = "wcsfilecmp"]
fn wcsfilecmp_ffi(a: wcharz_t, b: wcharz_t) -> i32;
#[cxx_name = "wcsfilecmp_glob"]
fn wcsfilecmp_glob_ffi(a: wcharz_t, b: wcharz_t) -> i32;
fn get_time() -> i64;
}
}
fn ordering_to_int(ord: Ordering) -> i32 {
match ord {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}
}
fn wcsfilecmp_glob_ffi(a: wcharz_t, b: wcharz_t) -> i32 {
ordering_to_int(wcsfilecmp_glob(a.into(), b.into()))
}
fn wcsfilecmp_ffi(a: wcharz_t, b: wcharz_t) -> i32 {
ordering_to_int(wcsfilecmp(a.into(), b.into()))
}
/// Compares two wide character strings with an (arguably) intuitive ordering. This function tries
/// to order strings in a way which is intuitive to humans with regards to sorting strings
/// containing numbers.
///
/// Most sorting functions would sort the strings 'file1.txt' 'file5.txt' and 'file12.txt' as:
///
/// file1.txt
/// file12.txt
/// file5.txt
///
/// This function regards any sequence of digits as a single entity when performing comparisons, so
/// the output is instead:
///
/// file1.txt
/// file5.txt
/// file12.txt
///
/// Which most people would find more intuitive.
///
/// This won't return the optimum results for numbers in bases higher than ten, such as hexadecimal,
/// but at least a stable sort order will result.
///
/// This function performs a two-tiered sort, where difference in case and in number of leading
/// zeroes in numbers only have effect if no other differences between strings are found. This way,
/// a 'file1' and 'File1' will not be considered identical, and hence their internal sort order is
/// not arbitrary, but the names 'file1', 'File2' and 'file3' will still be sorted in the order
/// given above.
pub fn wcsfilecmp(a: &wstr, b: &wstr) -> Ordering {
let mut retval = Ordering::Equal;
let mut ai = 0;
let mut bi = 0;
while ai < a.len() && bi < b.len() {
let ac = a.as_char_slice()[ai];
let bc = b.as_char_slice()[bi];
if ac.is_ascii_digit() && bc.is_ascii_digit() {
let (ad, bd);
(retval, ad, bd) = wcsfilecmp_leading_digits(&a[ai..], &b[bi..]);
ai += ad;
bi += bd;
if retval != Ordering::Equal || ai == a.len() || bi == b.len() {
break;
}
continue;
}
// Fast path: Skip towupper.
if ac == bc {
ai += 1;
bi += 1;
continue;
}
// Sort dashes after Z - see #5634
let mut acl = if ac == '-' { '[' } else { ac };
let mut bcl = if bc == '-' { '[' } else { bc };
// TODO Compare the tail (enabled by Rust's Unicode support).
acl = acl.to_uppercase().next().unwrap();
bcl = bcl.to_uppercase().next().unwrap();
match acl.cmp(&bcl) {
Ordering::Equal => {
ai += 1;
bi += 1;
}
o => {
retval = o;
break;
}
}
}
if retval != Ordering::Equal {
return retval; // we already know the strings aren't logically equal
}
if ai == a.len() {
if bi == b.len() {
// The strings are logically equal. They may or may not be the same length depending on
// whether numbers were present but that doesn't matter. Disambiguate strings that
// differ by letter case or length. We don't bother optimizing the case where the file
// names are literally identical because that won't occur given how this function is
// used. And even if it were to occur (due to being reused in some other context) it
// would be so rare that it isn't worth optimizing for.
a.cmp(b)
} else {
Ordering::Less // string a is a prefix of b and b is longer
}
} else {
assert!(bi == b.len());
Ordering::Greater // string b is a prefix of a and a is longer
}
}
/// wcsfilecmp, but frozen in time for glob usage.
pub fn wcsfilecmp_glob(a: &wstr, b: &wstr) -> Ordering {
let mut retval = Ordering::Equal;
let mut ai = 0;
let mut bi = 0;
while ai < a.len() && bi < b.len() {
let ac = a.as_char_slice()[ai];
let bc = b.as_char_slice()[bi];
if ac.is_ascii_digit() && bc.is_ascii_digit() {
let (ad, bd);
(retval, ad, bd) = wcsfilecmp_leading_digits(&a[ai..], &b[bi..]);
ai += ad;
bi += bd;
// If we know the strings aren't logically equal or we've reached the end of one or both
// strings we can stop iterating over the chars in each string.
if retval != Ordering::Equal || ai == a.len() || bi == b.len() {
break;
}
continue;
}
// Fast path: Skip towlower.
if ac == bc {
ai += 1;
bi += 1;
continue;
}
// TODO Compare the tail (enabled by Rust's Unicode support).
let acl = ac.to_lowercase().next().unwrap();
let bcl = bc.to_lowercase().next().unwrap();
match acl.cmp(&bcl) {
Ordering::Equal => {
ai += 1;
bi += 1;
}
o => {
retval = o;
break;
}
}
}
if retval != Ordering::Equal {
return retval; // we already know the strings aren't logically equal
}
if ai == a.len() {
if bi == b.len() {
// The strings are logically equal. They may or may not be the same length depending on
// whether numbers were present but that doesn't matter. Disambiguate strings that
// differ by letter case or length. We don't bother optimizing the case where the file
// names are literally identical because that won't occur given how this function is
// used. And even if it were to occur (due to being reused in some other context) it
// would be so rare that it isn't worth optimizing for.
a.cmp(b)
} else {
Ordering::Less // string a is a prefix of b and b is longer
}
} else {
assert!(bi == b.len());
Ordering::Greater // string b is a prefix of a and a is longer
}
}
/// Get the current time in microseconds since Jan 1, 1970.
pub fn get_time() -> i64 {
match time::SystemTime::now().duration_since(time::UNIX_EPOCH) {
Ok(difference) => difference.as_micros() as i64,
Err(until_epoch) => -(until_epoch.duration().as_micros() as i64),
}
}
// Compare the strings to see if they begin with an integer that can be compared and return the
// result of that comparison.
fn wcsfilecmp_leading_digits(a: &wstr, b: &wstr) -> (Ordering, usize, usize) {
// Ignore leading 0s.
let mut ai = a.as_char_slice().iter().take_while(|c| **c == '0').count();
let mut bi = b.as_char_slice().iter().take_while(|c| **c == '0').count();
let mut ret = Ordering::Equal;
loop {
let ac = a.as_char_slice().get(ai).unwrap_or(&'\0');
let bc = b.as_char_slice().get(bi).unwrap_or(&'\0');
if ac.is_ascii_digit() && bc.is_ascii_digit() {
// We keep the cmp value for the
// first differing digit.
//
// If the numbers have the same length, that's the value.
if ret == Ordering::Equal {
// Comparing the string value is the same as numerical
// for wchar_t digits!
ret = ac.cmp(bc);
}
} else {
// We don't have negative numbers and we only allow ints,
// and we have already skipped leading zeroes,
// so the longer number is larger automatically.
if ac.is_ascii_digit() {
ret = Ordering::Greater;
}
if bc.is_ascii_digit() {
ret = Ordering::Less;
}
break;
}
ai += 1;
bi += 1;
}
// For historical reasons, we skip trailing whitespace
// like fish_wcstol does!
// This is used in sorting globs, and that's supposed to be stable.
ai += a
.as_char_slice()
.iter()
.skip(ai)
.take_while(|c| c.is_whitespace())
.count();
bi += b
.as_char_slice()
.iter()
.skip(bi)
.take_while(|c| c.is_whitespace())
.count();
(ret, ai, bi)
}
/// Verify the behavior of the `wcsfilecmp()` function.
#[test]
fn test_wcsfilecmp() {
use crate::wchar::L;
macro_rules! validate {
($str1:expr, $str2:expr, $expected_rc:expr) => {
assert_eq!(wcsfilecmp(L!($str1), L!($str2)), $expected_rc)
};
}
// Not using L as suffix because the macro munges error locations.
validate!("", "", Ordering::Equal);
validate!("", "def", Ordering::Less);
validate!("abc", "", Ordering::Greater);
validate!("abc", "def", Ordering::Less);
validate!("abc", "DEF", Ordering::Less);
validate!("DEF", "abc", Ordering::Greater);
validate!("abc", "abc", Ordering::Equal);
validate!("ABC", "ABC", Ordering::Equal);
validate!("AbC", "abc", Ordering::Less);
validate!("AbC", "ABC", Ordering::Greater);
validate!("def", "abc", Ordering::Greater);
validate!("1ghi", "1gHi", Ordering::Greater);
validate!("1ghi", "2ghi", Ordering::Less);
validate!("1ghi", "01ghi", Ordering::Greater);
validate!("1ghi", "02ghi", Ordering::Less);
validate!("01ghi", "1ghi", Ordering::Less);
validate!("1ghi", "002ghi", Ordering::Less);
validate!("002ghi", "1ghi", Ordering::Greater);
validate!("abc01def", "abc1def", Ordering::Less);
validate!("abc1def", "abc01def", Ordering::Greater);
validate!("abc12", "abc5", Ordering::Greater);
validate!("51abc", "050abc", Ordering::Greater);
validate!("abc5", "abc12", Ordering::Less);
validate!("5abc", "12ABC", Ordering::Less);
validate!("abc0789", "abc789", Ordering::Less);
validate!("abc0xA789", "abc0xA0789", Ordering::Greater);
validate!("abc002", "abc2", Ordering::Less);
validate!("abc002g", "abc002", Ordering::Greater);
validate!("abc002g", "abc02g", Ordering::Less);
validate!("abc002.txt", "abc02.txt", Ordering::Less);
validate!("abc005", "abc012", Ordering::Less);
validate!("abc02", "abc002", Ordering::Greater);
validate!("abc002.txt", "abc02.txt", Ordering::Less);
validate!("GHI1abc2.txt", "ghi1abc2.txt", Ordering::Less);
validate!("a0", "a00", Ordering::Less);
validate!("a00b", "a0b", Ordering::Less);
validate!("a0b", "a00b", Ordering::Greater);
validate!("a-b", "azb", Ordering::Greater);
}

View File

@ -1,80 +0,0 @@
//! Support for wide strings.
//!
//! There are two wide string types that are commonly used:
//! - wstr: a string slice without a nul terminator. Like `&str` but wide chars.
//! - WString: an owning string without a nul terminator. Like `String` but wide chars.
pub use widestring::{Utf32Str as wstr, Utf32String as WString};
/// Creates a wstr string slice, like the "L" prefix of C++.
/// The result is of type wstr.
/// It is NOT nul-terminated.
macro_rules! L {
($string:expr) => {
widestring::utf32str!($string)
};
}
pub(crate) use L;
/// A proc-macro for creating wide string literals using an L *suffix*.
/// Example usage:
/// ```
/// #[widestrs]
/// pub fn func() {
/// let s = "hello"L; // type &'static wstr
/// }
/// ```
/// Note: the resulting string is NOT nul-terminated.
pub use widestring_suffix::widestrs;
/// Pull in our extensions.
pub use crate::wchar_ext::{CharPrefixSuffix, WExt};
// Use Unicode "non-characters" for internal characters as much as we can. This
// gives us 32 "characters" for internal use that we can guarantee should not
// appear in our input stream. See http://www.unicode.org/faq/private_use.html.
pub const RESERVED_CHAR_BASE: char = '\u{FDD0}';
pub const RESERVED_CHAR_END: char = '\u{FDF0}';
// Split the available non-character values into two ranges to ensure there are
// no conflicts among the places we use these special characters.
pub const EXPAND_RESERVED_BASE: char = RESERVED_CHAR_BASE;
pub const EXPAND_RESERVED_END: char = match char::from_u32(EXPAND_RESERVED_BASE as u32 + 16u32) {
Some(c) => c,
None => panic!("private use codepoint in expansion region should be valid char"),
};
pub const WILDCARD_RESERVED_BASE: char = EXPAND_RESERVED_END;
pub const WILDCARD_RESERVED_END: char = match char::from_u32(WILDCARD_RESERVED_BASE as u32 + 16u32)
{
Some(c) => c,
None => panic!("private use codepoint in wildcard region should be valid char"),
};
// These are in the Unicode private-use range. We really shouldn't use this
// range but have little choice in the matter given how our lexer/parser works.
// We can't use non-characters for these two ranges because there are only 66 of
// them and we need at least 256 + 64.
//
// If sizeof(wchar_t)==4 we could avoid using private-use chars; however, that
// would result in fish having different behavior on machines with 16 versus 32
// bit wchar_t. It's better that fish behave the same on both types of systems.
//
// Note: We don't use the highest 8 bit range (0xF800 - 0xF8FF) because we know
// of at least one use of a codepoint in that range: the Apple symbol (0xF8FF)
// on Mac OS X. See http://www.unicode.org/faq/private_use.html.
const ENCODE_DIRECT_BASE: char = '\u{F600}';
const ENCODE_DIRECT_END: char = match char::from_u32(ENCODE_DIRECT_BASE as u32 + 256) {
Some(c) => c,
None => panic!("private use codepoint in encode direct region should be valid char"),
};
/// Encode a literal byte in a UTF-32 character. This is required for e.g. the echo builtin, whose
/// escape sequences can be used to construct raw byte sequences which are then interpreted as e.g.
/// UTF-8 by the terminal. If we were to interpret each of those bytes as a codepoint and encode it
/// as a UTF-32 character, printing them would result in several characters instead of one UTF-8
/// character.
///
/// See https://github.com/fish-shell/fish-shell/issues/1894.
pub fn wchar_literal_byte(byte: u8) -> char {
char::from_u32(u32::from(ENCODE_DIRECT_BASE) + u32::from(byte))
.expect("private-use codepoint should be valid char")
}

View File

@ -1,137 +0,0 @@
use crate::wchar::{wstr, WString};
use widestring::utfstr::CharsUtf32;
/// A thing that a wide string can start with or end with.
/// It must have a chars() method which returns a double-ended char iterator.
pub trait CharPrefixSuffix {
type Iter: DoubleEndedIterator<Item = char>;
fn chars(self) -> Self::Iter;
}
impl CharPrefixSuffix for char {
type Iter = std::iter::Once<char>;
fn chars(self) -> Self::Iter {
std::iter::once(self)
}
}
impl<'a> CharPrefixSuffix for &'a str {
type Iter = std::str::Chars<'a>;
fn chars(self) -> Self::Iter {
str::chars(self)
}
}
impl<'a> CharPrefixSuffix for &'a wstr {
type Iter = CharsUtf32<'a>;
fn chars(self) -> Self::Iter {
wstr::chars(self)
}
}
impl<'a> CharPrefixSuffix for &'a WString {
type Iter = CharsUtf32<'a>;
fn chars(self) -> Self::Iter {
wstr::chars(self)
}
}
/// \return true if \p prefix is a prefix of \p contents.
fn iter_prefixes_iter<Prefix, Contents>(prefix: Prefix, mut contents: Contents) -> bool
where
Prefix: Iterator,
Contents: Iterator,
Prefix::Item: PartialEq<Contents::Item>,
{
for c1 in prefix {
match contents.next() {
Some(c2) if c1 == c2 => {}
_ => return false,
}
}
true
}
/// Convenience functions for WString.
pub trait WExt {
/// Access the chars of a WString or wstr.
fn as_char_slice(&self) -> &[char];
/// \return the char at an index.
/// If the index is equal to the length, return '\0'.
/// If the index exceeds the length, then panic.
fn char_at(&self, index: usize) -> char {
let chars = self.as_char_slice();
if index == chars.len() {
'\0'
} else {
chars[index]
}
}
/// \return the index of the first occurrence of the given char, or None.
fn find_char(&self, c: char) -> Option<usize> {
self.as_char_slice().iter().position(|&x| x == c)
}
/// \return whether we start with a given Prefix.
/// The Prefix can be a char, a &str, a &wstr, or a &WString.
fn starts_with<Prefix: CharPrefixSuffix>(&self, prefix: Prefix) -> bool {
iter_prefixes_iter(prefix.chars(), self.as_char_slice().iter().copied())
}
/// \return whether we end with a given Suffix.
/// The Suffix can be a char, a &str, a &wstr, or a &WString.
fn ends_with<Suffix: CharPrefixSuffix>(&self, suffix: Suffix) -> bool {
iter_prefixes_iter(
suffix.chars().rev(),
self.as_char_slice().iter().copied().rev(),
)
}
}
impl WExt for WString {
fn as_char_slice(&self) -> &[char] {
self.as_utfstr().as_char_slice()
}
}
impl WExt for wstr {
fn as_char_slice(&self) -> &[char] {
wstr::as_char_slice(self)
}
}
#[cfg(test)]
mod tests {
use super::WExt;
use crate::wchar::{WString, L};
/// Write some tests.
#[cfg(test)]
fn test_find_char() {
assert_eq!(Some(0), L!("abc").find_char('a'));
assert_eq!(Some(1), L!("abc").find_char('b'));
assert_eq!(None, L!("abc").find_char('X'));
assert_eq!(None, L!("").find_char('X'));
}
#[cfg(test)]
fn test_prefix() {
assert!(L!("").starts_with(L!("")));
assert!(L!("abc").starts_with(L!("")));
assert!(L!("abc").starts_with('a'));
assert!(L!("abc").starts_with("ab"));
assert!(L!("abc").starts_with(L!("ab")));
assert!(L!("abc").starts_with(&WString::from_str("abc")));
}
#[cfg(test)]
fn test_suffix() {
assert!(L!("").ends_with(L!("")));
assert!(L!("abc").ends_with(L!("")));
assert!(L!("abc").ends_with('c'));
assert!(L!("abc").ends_with("bc"));
assert!(L!("abc").ends_with(L!("bc")));
assert!(L!("abc").ends_with(&WString::from_str("abc")));
}
}

View File

@ -1,172 +0,0 @@
//! Interfaces for various FFI string types.
//!
//! We have the following string types for FFI purposes:
//! - CxxWString: the Rust view of a C++ wstring.
//! - W0String: an owning string with a nul terminator.
//! - wcharz_t: a "newtyped" pointer to a nul-terminated string, implemented in C++.
//! This is useful for FFI boundaries, to work around autocxx limitations on pointers.
pub use crate::ffi::{wchar_t, wcharz_t};
use crate::wchar::{wstr, WString};
use once_cell::sync::Lazy;
pub use widestring::u32cstr;
pub use widestring::U32CString as W0String;
/// \return the length of a nul-terminated raw string.
pub fn wcslen(str: *const wchar_t) -> usize {
assert!(!str.is_null(), "Null pointer");
let mut len = 0;
unsafe {
while *str.offset(len) != 0 {
len += 1;
}
}
len as usize
}
impl wcharz_t {
/// \return the chars of a wcharz_t.
pub fn chars(&self) -> &[char] {
assert!(!self.str_.is_null(), "Null wcharz");
let data = self.str_ as *const char;
let len = self.size();
unsafe { std::slice::from_raw_parts(data, len) }
}
}
/// Convert wcharz_t to an WString.
impl From<&wcharz_t> for WString {
fn from(wcharz: &wcharz_t) -> Self {
WString::from_chars(wcharz.chars())
}
}
/// Convert a wstr or WString to a W0String, which contains a nul-terminator.
/// This is useful for passing across FFI boundaries.
/// In general you don't need to use this directly - use the c_str macro below.
pub fn wstr_to_u32string<Str: AsRef<wstr>>(str: Str) -> W0String {
W0String::from_ustr(str.as_ref()).expect("String contained intermediate NUL character")
}
/// Convert a wstr to a nul-terminated pointer.
/// This needs to be a macro so we can create a temporary with the proper lifetime.
macro_rules! c_str {
($string:expr) => {
crate::wchar_ffi::wstr_to_u32string($string)
.as_ucstr()
.as_ptr()
.cast::<crate::ffi::wchar_t>()
};
}
/// Convert a wstr to a wcharz_t.
macro_rules! wcharz {
($string:expr) => {
crate::wchar_ffi::wcharz_t {
str_: crate::wchar_ffi::c_str!($string),
}
};
}
pub(crate) use c_str;
pub(crate) use wcharz;
static EMPTY_WSTRING: Lazy<cxx::UniquePtr<cxx::CxxWString>> =
Lazy::new(|| cxx::CxxWString::create(&[]));
/// \return a reference to a shared empty wstring.
pub fn empty_wstring() -> &'static cxx::CxxWString {
&EMPTY_WSTRING
}
/// Implement Debug for wcharz_t.
impl std::fmt::Debug for wcharz_t {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.str_.is_null() {
write!(f, "((null))")
} else {
self.chars().fmt(f)
}
}
}
/// Convert self to a CxxWString, in preparation for using over FFI.
/// We can't use "From" as WString is implemented in an external crate.
pub trait WCharToFFI {
fn to_ffi(&self) -> cxx::UniquePtr<cxx::CxxWString>;
}
/// WString may be converted to CxxWString.
impl WCharToFFI for WString {
fn to_ffi(&self) -> cxx::UniquePtr<cxx::CxxWString> {
cxx::CxxWString::create(self.as_char_slice())
}
}
/// wstr (wide string slices) may be converted to CxxWString.
impl WCharToFFI for wstr {
fn to_ffi(&self) -> cxx::UniquePtr<cxx::CxxWString> {
cxx::CxxWString::create(self.as_char_slice())
}
}
/// wcharz_t (wide char) may be converted to CxxWString.
impl WCharToFFI for wcharz_t {
fn to_ffi(&self) -> cxx::UniquePtr<cxx::CxxWString> {
cxx::CxxWString::create(self.chars())
}
}
/// Convert from a CxxWString, in preparation for using over FFI.
pub trait WCharFromFFI<Target> {
/// Convert from a CxxWString for FFI purposes.
#[allow(clippy::wrong_self_convention)]
fn from_ffi(&self) -> Target;
}
impl WCharFromFFI<WString> for cxx::CxxWString {
fn from_ffi(&self) -> WString {
WString::from_chars(self.as_chars())
}
}
impl WCharFromFFI<WString> for cxx::UniquePtr<cxx::CxxWString> {
fn from_ffi(&self) -> WString {
WString::from_chars(self.as_chars())
}
}
impl WCharFromFFI<WString> for cxx::SharedPtr<cxx::CxxWString> {
fn from_ffi(&self) -> WString {
WString::from_chars(self.as_chars())
}
}
impl WCharFromFFI<Vec<u8>> for cxx::UniquePtr<cxx::CxxString> {
fn from_ffi(&self) -> Vec<u8> {
self.as_bytes().to_vec()
}
}
impl WCharFromFFI<Vec<u8>> for cxx::SharedPtr<cxx::CxxString> {
fn from_ffi(&self) -> Vec<u8> {
self.as_bytes().to_vec()
}
}
/// Convert from FFI types to a reference to a wide string (i.e. a [`wstr`]) without allocating.
pub trait AsWstr<'a> {
fn as_wstr(&'a self) -> &'a wstr;
}
impl<'a> AsWstr<'a> for cxx::UniquePtr<cxx::CxxWString> {
fn as_wstr(&'a self) -> &'a wstr {
wstr::from_char_slice(self.as_chars())
}
}
impl<'a> AsWstr<'a> for cxx::CxxWString {
fn as_wstr(&'a self) -> &'a wstr {
wstr::from_char_slice(self.as_chars())
}
}

View File

@ -1,616 +0,0 @@
//! A version of the getopt library for use with wide character strings.
//!
//! Note wgetopter expects an mutable array of const strings. It modifies the order of the
//! strings, but not their contents.
/* Declarations for getopt.
Copyright (C) 1989, 90, 91, 92, 93, 94 Free Software Foundation, Inc.
This file is part of the GNU C Library. Its master source is NOT part of
the C library, however. The master source lives in /gd/gnu/lib.
The GNU C Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.
The GNU C Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with the GNU C Library; see the file COPYING.LIB. If
not, write to the Free Software Foundation, Inc., 675 Mass Ave,
Cambridge, MA 02139, USA. */
use crate::wchar::{wstr, WExt, L};
/// Describe how to deal with options that follow non-option ARGV-elements.
///
/// If the caller did not specify anything, the default is PERMUTE.
///
/// REQUIRE_ORDER means don't recognize them as options; stop option processing when the first
/// non-option is seen. This is what Unix does. This mode of operation is selected by using `+'
/// as the first character of the list of option characters.
///
/// PERMUTE is the default. We permute the contents of ARGV as we scan, so that eventually all
/// the non-options are at the end. This allows options to be given in any order, even with
/// programs that were not written to expect this.
///
/// RETURN_IN_ORDER is an option available to programs that were written to expect options and
/// other ARGV-elements in any order and that care about the ordering of the two. We describe
/// each non-option ARGV-element as if it were the argument of an option with character code 1.
/// Using `-` as the first character of the list of option characters selects this mode of
/// operation.
///
/// The special argument `--` forces an end of option-scanning regardless of the value of
/// `ordering`. In the case of RETURN_IN_ORDER, only `--` can cause `getopt` to return EOF with
/// `woptind` != ARGC.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::upper_case_acronyms)]
enum Ordering {
REQUIRE_ORDER,
PERMUTE,
RETURN_IN_ORDER,
}
impl Default for Ordering {
fn default() -> Self {
Ordering::PERMUTE
}
}
fn empty_wstr() -> &'static wstr {
Default::default()
}
pub struct wgetopter_t<'opts, 'args, 'argarray> {
/// Argv.
argv: &'argarray mut [&'args wstr],
/// For communication from `getopt` to the caller. When `getopt` finds an option that takes an
/// argument, the argument value is returned here. Also, when `ordering` is RETURN_IN_ORDER, each
/// non-option ARGV-element is returned here.
pub woptarg: Option<&'args wstr>,
shortopts: &'opts wstr,
longopts: &'opts [woption<'opts>],
/// The next char to be scanned in the option-element in which the last option character we
/// returned was found. This allows us to pick up the scan where we left off.
///
/// If this is empty, it means resume the scan by advancing to the next ARGV-element.
nextchar: &'args wstr,
/// Index in ARGV of the next element to be scanned. This is used for communication to and from
/// the caller and for communication between successive calls to `getopt`.
///
/// On entry to `getopt`, zero means this is the first call; initialize.
///
/// When `getopt` returns EOF, this is the index of the first of the non-option elements that the
/// caller should itself scan.
///
/// Otherwise, `woptind` communicates from one call to the next how much of ARGV has been scanned
/// so far.
// XXX 1003.2 says this must be 1 before any call.
pub woptind: usize,
/// Set to an option character which was unrecognized.
woptopt: char,
/// Describe how to deal with options that follow non-option ARGV-elements.
ordering: Ordering,
/// Handle permutation of arguments.
///
/// Describe the part of ARGV that contains non-options that have been skipped. `first_nonopt`
/// is the index in ARGV of the first of them; `last_nonopt` is the index after the last of them.
pub first_nonopt: usize,
pub last_nonopt: usize,
missing_arg_return_colon: bool,
initialized: bool,
}
/// Names for the values of the `has_arg` field of `woption`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum woption_argument_t {
no_argument,
required_argument,
optional_argument,
}
/// Describe the long-named options requested by the application. The LONG_OPTIONS argument to
/// getopt_long or getopt_long_only is a vector of `struct option' terminated by an element
/// containing a name which is zero.
///
/// The field `has_arg` is:
/// no_argument (or 0) if the option does not take an argument,
/// required_argument (or 1) if the option requires an argument,
/// optional_argument (or 2) if the option takes an optional argument.
///
/// If the field `flag` is not NULL, it points to a variable that is set to the value given in the
/// field `val` when the option is found, but left unchanged if the option is not found.
///
/// To have a long-named option do something other than set an `int` to a compiled-in constant, such
/// as set a value from `optarg`, set the option's `flag` field to zero and its `val` field to a
/// nonzero value (the equivalent single-letter option character, if there is one). For long
/// options that have a zero `flag` field, `getopt` returns the contents of the `val` field.
#[derive(Debug, Clone, Copy)]
pub struct woption<'a> {
/// Long name for switch.
pub name: &'a wstr,
pub has_arg: woption_argument_t,
/// If \c flag is non-null, this is the value that flag will be set to. Otherwise, this is the
/// return-value of the function call.
pub val: char,
}
/// Helper function to create a woption.
pub const fn wopt(name: &wstr, has_arg: woption_argument_t, val: char) -> woption<'_> {
woption { name, has_arg, val }
}
impl<'opts, 'args, 'argarray> wgetopter_t<'opts, 'args, 'argarray> {
pub fn new(
shortopts: &'opts wstr,
longopts: &'opts [woption],
argv: &'argarray mut [&'args wstr],
) -> Self {
return wgetopter_t {
woptopt: '?',
argv,
shortopts,
longopts,
first_nonopt: 0,
initialized: false,
last_nonopt: 0,
missing_arg_return_colon: false,
nextchar: Default::default(),
ordering: Ordering::PERMUTE,
woptarg: None,
woptind: 0,
};
}
pub fn wgetopt_long(&mut self) -> Option<char> {
assert!(self.woptind <= self.argc(), "woptind is out of range");
let mut ignored = 0;
return self._wgetopt_internal(&mut ignored, false);
}
pub fn wgetopt_long_idx(&mut self, opt_index: &mut usize) -> Option<char> {
return self._wgetopt_internal(opt_index, false);
}
/// \return the number of arguments.
fn argc(&self) -> usize {
return self.argv.len();
}
/// Exchange two adjacent subsequences of ARGV. One subsequence is elements
/// [first_nonopt,last_nonopt) which contains all the non-options that have been skipped so far. The
/// other is elements [last_nonopt,woptind), which contains all the options processed since those
/// non-options were skipped.
///
/// `first_nonopt` and `last_nonopt` are relocated so that they describe the new indices of the
/// non-options in ARGV after they are moved.
fn exchange(&mut self) {
let mut bottom = self.first_nonopt;
let middle = self.last_nonopt;
let mut top = self.woptind;
// Exchange the shorter segment with the far end of the longer segment. That puts the shorter
// segment into the right place. It leaves the longer segment in the right place overall, but it
// consists of two parts that need to be swapped next.
while top > middle && middle > bottom {
if top - middle > middle - bottom {
// Bottom segment is the short one.
let len = middle - bottom;
// Swap it with the top part of the top segment.
for i in 0..len {
self.argv.swap(bottom + i, top - (middle - bottom) + i);
}
// Exclude the moved bottom segment from further swapping.
top -= len;
} else {
// Top segment is the short one.
let len = top - middle;
// Swap it with the bottom part of the bottom segment.
for i in 0..len {
self.argv.swap(bottom + i, middle + i);
}
// Exclude the moved top segment from further swapping.
bottom += len;
}
}
// Update records for the slots the non-options now occupy.
self.first_nonopt += self.woptind - self.last_nonopt;
self.last_nonopt = self.woptind;
}
/// Initialize the internal data when the first call is made.
fn _wgetopt_initialize(&mut self) {
// Start processing options with ARGV-element 1 (since ARGV-element 0 is the program name); the
// sequence of previously skipped non-option ARGV-elements is empty.
self.first_nonopt = 1;
self.last_nonopt = 1;
self.woptind = 1;
self.nextchar = empty_wstr();
let mut optstring = self.shortopts;
// Determine how to handle the ordering of options and nonoptions.
if optstring.char_at(0) == '-' {
self.ordering = Ordering::RETURN_IN_ORDER;
optstring = &optstring[1..];
} else if optstring.char_at(0) == '+' {
self.ordering = Ordering::REQUIRE_ORDER;
optstring = &optstring[1..];
} else {
self.ordering = Ordering::PERMUTE;
}
if optstring.char_at(0) == ':' {
self.missing_arg_return_colon = true;
optstring = &optstring[1..];
}
self.shortopts = optstring;
self.initialized = true;
}
/// Advance to the next ARGV-element.
/// \return Some(\0) on success, or None or another value if we should stop.
fn _advance_to_next_argv(&mut self) -> Option<char> {
let argc = self.argc();
if self.ordering == Ordering::PERMUTE {
// If we have just processed some options following some non-options, exchange them so
// that the options come first.
if self.first_nonopt != self.last_nonopt && self.last_nonopt != self.woptind {
self.exchange();
} else if self.last_nonopt != self.woptind {
self.first_nonopt = self.woptind;
}
// Skip any additional non-options and extend the range of non-options previously
// skipped.
while self.woptind < argc
&& (self.argv[self.woptind].char_at(0) != '-' || self.argv[self.woptind].len() == 1)
{
self.woptind += 1;
}
self.last_nonopt = self.woptind;
}
// The special ARGV-element `--' means premature end of options. Skip it like a null option,
// then exchange with previous non-options as if it were an option, then skip everything
// else like a non-option.
if self.woptind != argc && self.argv[self.woptind] == "--" {
self.woptind += 1;
if self.first_nonopt != self.last_nonopt && self.last_nonopt != self.woptind {
self.exchange();
} else if self.first_nonopt == self.last_nonopt {
self.first_nonopt = self.woptind;
}
self.last_nonopt = argc;
self.woptind = argc;
}
// If we have done all the ARGV-elements, stop the scan and back over any non-options that
// we skipped and permuted.
if self.woptind == argc {
// Set the next-arg-index to point at the non-options that we previously skipped, so the
// caller will digest them.
if self.first_nonopt != self.last_nonopt {
self.woptind = self.first_nonopt;
}
return None;
}
// If we have come to a non-option and did not permute it, either stop the scan or describe
// it to the caller and pass it by.
if self.argv[self.woptind].char_at(0) != '-' || self.argv[self.woptind].len() == 1 {
if self.ordering == Ordering::REQUIRE_ORDER {
return None;
}
self.woptarg = Some(self.argv[self.woptind]);
self.woptind += 1;
return Some(char::from(1));
}
// We have found another option-ARGV-element. Skip the initial punctuation.
let skip = if !self.longopts.is_empty() && self.argv[self.woptind].char_at(1) == '-' {
2
} else {
1
};
self.nextchar = self.argv[self.woptind][skip..].into();
return Some(char::from(0));
}
/// Check for a matching short opt.
fn _handle_short_opt(&mut self) -> char {
// Look at and handle the next short option-character.
let mut c = self.nextchar.char_at(0);
self.nextchar = &self.nextchar[1..];
let temp = match self.shortopts.chars().position(|sc| sc == c) {
Some(pos) => &self.shortopts[pos..],
None => L!(""),
};
// Increment `woptind' when we start to process its last character.
if self.nextchar.is_empty() {
self.woptind += 1;
}
if temp.is_empty() || c == ':' {
self.woptopt = c;
if !self.nextchar.is_empty() {
self.woptind += 1;
}
return '?';
}
if temp.char_at(1) != ':' {
return c;
}
if temp.char_at(2) == ':' {
// This is an option that accepts an argument optionally.
if !self.nextchar.is_empty() {
self.woptarg = Some(self.nextchar);
self.woptind += 1;
} else {
self.woptarg = None;
}
self.nextchar = empty_wstr();
} else {
// This is an option that requires an argument.
if !self.nextchar.is_empty() {
self.woptarg = Some(self.nextchar);
// If we end this ARGV-element by taking the rest as an arg, we must advance to
// the next element now.
self.woptind += 1;
} else if self.woptind == self.argc() {
self.woptopt = c;
c = if self.missing_arg_return_colon {
':'
} else {
'?'
};
} else {
// We already incremented `woptind' once; increment it again when taking next
// ARGV-elt as argument.
self.woptarg = Some(self.argv[self.woptind]);
self.woptind += 1;
}
self.nextchar = empty_wstr();
}
return c;
}
fn _update_long_opt(
&mut self,
pfound: &woption,
nameend: usize,
longind: &mut usize,
option_index: usize,
retval: &mut char,
) {
self.woptind += 1;
assert!(self.nextchar.char_at(nameend) == '\0' || self.nextchar.char_at(nameend) == '=');
if self.nextchar.char_at(nameend) == '=' {
if pfound.has_arg != woption_argument_t::no_argument {
self.woptarg = Some(self.nextchar[(nameend + 1)..].into());
} else {
self.nextchar = empty_wstr();
*retval = '?';
return;
}
} else if pfound.has_arg == woption_argument_t::required_argument {
if self.woptind < self.argc() {
self.woptarg = Some(self.argv[self.woptind]);
self.woptind += 1;
} else {
self.nextchar = empty_wstr();
*retval = if self.missing_arg_return_colon {
':'
} else {
'?'
};
return;
}
}
self.nextchar = empty_wstr();
*longind = option_index;
*retval = pfound.val;
}
/// Find a matching long opt.
fn _find_matching_long_opt(
&self,
nameend: usize,
exact: &mut bool,
ambig: &mut bool,
indfound: &mut usize,
) -> Option<woption<'opts>> {
let mut pfound: Option<woption> = None;
// Test all long options for either exact match or abbreviated matches.
for (option_index, p) in self.longopts.iter().enumerate() {
// Check if current option is prefix of long opt
if p.name.starts_with(&self.nextchar[..nameend]) {
if nameend == p.name.len() {
// The current option is exact match of this long option
pfound = Some(*p);
*indfound = option_index;
*exact = true;
break;
} else if pfound.is_none() {
// current option is first prefix match but not exact match
pfound = Some(*p);
*indfound = option_index;
} else {
// current option is second or later prefix match but not exact match
*ambig = true;
}
}
}
return pfound;
}
/// Check for a matching long opt.
fn _handle_long_opt(
&mut self,
longind: &mut usize,
long_only: bool,
retval: &mut char,
) -> bool {
let mut exact = false;
let mut ambig = false;
let mut indfound: usize = 0;
let mut nameend = 0;
while self.nextchar.char_at(nameend) != '\0' && self.nextchar.char_at(nameend) != '=' {
nameend += 1;
}
let pfound = self._find_matching_long_opt(nameend, &mut exact, &mut ambig, &mut indfound);
if ambig && !exact {
self.nextchar = empty_wstr();
self.woptind += 1;
*retval = '?';
return true;
}
if let Some(pfound) = pfound {
self._update_long_opt(&pfound, nameend, longind, indfound, retval);
return true;
}
// Can't find it as a long option. If this is not getopt_long_only, or the option starts
// with '--' or is not a valid short option, then it's an error. Otherwise interpret it as a
// short option.
if !long_only
|| self.argv[self.woptind].char_at(1) == '-'
|| !self
.shortopts
.as_char_slice()
.contains(&self.nextchar.char_at(0))
{
self.nextchar = empty_wstr();
self.woptind += 1;
*retval = '?';
return true;
}
return false;
}
/// Scan elements of ARGV (whose length is ARGC) for option characters given in OPTSTRING.
///
/// If an element of ARGV starts with '-', and is not exactly "-" or "--", then it is an option
/// element. The characters of this element (aside from the initial '-') are option characters. If
/// `getopt` is called repeatedly, it returns successively each of the option characters from each of
/// the option elements.
///
/// If `getopt` finds another option character, it returns that character, updating `woptind` and
/// `nextchar` so that the next call to `getopt` can resume the scan with the following option
/// character or ARGV-element.
///
/// If there are no more option characters, `getopt` returns `EOF`. Then `woptind` is the index in
/// ARGV of the first ARGV-element that is not an option. (The ARGV-elements have been permuted so
/// that those that are not options now come last.)
///
/// OPTSTRING is a string containing the legitimate option characters. If an option character is seen
/// that is not listed in OPTSTRING, return '?'.
///
/// If a char in OPTSTRING is followed by a colon, that means it wants an arg, so the following text
/// in the same ARGV-element, or the text of the following ARGV-element, is returned in `optarg`.
/// Two colons mean an option that wants an optional arg; if there is text in the current
/// ARGV-element, it is returned in `w.woptarg`, otherwise `w.woptarg` is set to zero.
///
/// If OPTSTRING starts with `-` or `+', it requests different methods of handling the non-option
/// ARGV-elements. See the comments about RETURN_IN_ORDER and REQUIRE_ORDER, above.
///
/// Long-named options begin with `--` instead of `-`. Their names may be abbreviated as long as the
/// abbreviation is unique or is an exact match for some defined option. If they have an argument,
/// it follows the option name in the same ARGV-element, separated from the option name by a `=', or
/// else the in next ARGV-element. When `getopt` finds a long-named option, it returns 0 if that
/// option's `flag` field is nonzero, the value of the option's `val` field if the `flag` field is
/// zero.
///
/// LONGOPTS is a vector of `struct option' terminated by an element containing a name which is zero.
///
/// LONGIND returns the index in LONGOPT of the long-named option found. It is only valid when a
/// long-named option has been found by the most recent call.
///
/// If LONG_ONLY is nonzero, '-' as well as '--' can introduce long-named options.
fn _wgetopt_internal(&mut self, longind: &mut usize, long_only: bool) -> Option<char> {
if !self.initialized {
self._wgetopt_initialize();
}
self.woptarg = None;
if self.nextchar.is_empty() {
let narg = self._advance_to_next_argv();
if narg != Some(char::from(0)) {
return narg;
}
}
// Decode the current option-ARGV-element.
// Check whether the ARGV-element is a long option.
//
// If long_only and the ARGV-element has the form "-f", where f is a valid short option, don't
// consider it an abbreviated form of a long option that starts with f. Otherwise there would
// be no way to give the -f short option.
//
// On the other hand, if there's a long option "fubar" and the ARGV-element is "-fu", do
// consider that an abbreviation of the long option, just like "--fu", and not "-f" with arg
// "u".
//
// This distinction seems to be the most useful approach.
if !self.longopts.is_empty() && self.woptind < self.argc() {
let arg = self.argv[self.woptind];
#[allow(clippy::if_same_then_else)]
#[allow(clippy::needless_bool)]
let try_long = if arg.char_at(0) == '-' && arg.char_at(1) == '-' {
// Like --foo
true
} else if long_only && arg.len() >= 3 {
// Like -fu
true
} else if !self.shortopts.as_char_slice().contains(&arg.char_at(1)) {
// Like -f, but f is not a short arg.
true
} else {
false
};
if try_long {
let mut retval = '\0';
if self._handle_long_opt(longind, long_only, &mut retval) {
return Some(retval);
}
}
}
return Some(self._handle_short_opt());
}
}

View File

@ -1,532 +0,0 @@
// Adapted from https://github.com/tjol/sprintf-rs
// License follows:
//
// Copyright (c) 2021 Thomas Jollans
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is furnished
// to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
// OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
use std::convert::{TryFrom, TryInto};
use super::parser::{ConversionSpecifier, ConversionType, NumericParam};
use super::printf::{PrintfError, Result};
use crate::wchar::{wstr, WExt, WString, L};
/// Trait for types that can be formatted using printf strings
///
/// Implemented for the basic types and shouldn't need implementing for
/// anything else.
pub trait Printf {
/// Format `self` based on the conversion configured in `spec`.
fn format(&self, spec: &ConversionSpecifier) -> Result<WString>;
/// Get `self` as an integer for use as a field width, if possible.
/// Defaults to None.
fn as_int(&self) -> Option<i32> {
None
}
}
impl Printf for u64 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
let mut base = 10;
let mut digits: Vec<char> = "0123456789".chars().collect();
let mut alt_prefix = L!("");
match spec.conversion_type {
ConversionType::DecInt => {}
ConversionType::HexIntLower => {
base = 16;
digits = "0123456789abcdef".chars().collect();
alt_prefix = L!("0x");
}
ConversionType::HexIntUpper => {
base = 16;
digits = "0123456789ABCDEF".chars().collect();
alt_prefix = L!("0X");
}
ConversionType::OctInt => {
base = 8;
digits = "01234567".chars().collect();
alt_prefix = L!("0");
}
_ => {
return Err(PrintfError::WrongType);
}
}
let prefix = if spec.alt_form {
alt_prefix.to_owned()
} else {
WString::new()
};
// Build the actual number (in reverse)
let mut rev_num = WString::new();
let mut n = *self;
while n > 0 {
let digit = n % base;
n /= base;
rev_num.push(digits[digit as usize]);
}
if rev_num.is_empty() {
rev_num.push('0');
}
// Take care of padding
let width: usize = match spec.width {
NumericParam::Literal(w) => w,
_ => {
return Err(PrintfError::Unknown); // should not happen at this point!!
}
}
.try_into()
.unwrap_or_default();
let formatted = if spec.left_adj {
let mut num_str = prefix;
num_str.extend(rev_num.chars().rev());
while num_str.len() < width {
num_str.push(' ');
}
num_str
} else if spec.zero_pad {
while prefix.len() + rev_num.len() < width {
rev_num.push('0');
}
let mut num_str = prefix;
num_str.extend(rev_num.chars().rev());
num_str
} else {
let mut num_str = prefix;
num_str.extend(rev_num.chars().rev());
while num_str.len() < width {
num_str.insert(0, ' ');
}
num_str
};
Ok(formatted)
}
fn as_int(&self) -> Option<i32> {
i32::try_from(*self).ok()
}
}
impl Printf for i64 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
match spec.conversion_type {
// signed integer format
ConversionType::DecInt => {
// do I need a sign prefix?
let negative = *self < 0;
let abs_val = self.abs();
let sign_prefix: &wstr = if negative {
L!("-")
} else if spec.force_sign {
L!("+")
} else if spec.space_sign {
L!(" ")
} else {
L!("")
};
let mut mod_spec = *spec;
mod_spec.width = match spec.width {
NumericParam::Literal(w) => NumericParam::Literal(w - sign_prefix.len() as i32),
_ => {
return Err(PrintfError::Unknown);
}
};
let formatted = (abs_val as u64).format(&mod_spec)?;
// put the sign a after any leading spaces
let mut actual_number = &formatted[0..];
let mut leading_spaces = &formatted[0..0];
if let Some(first_non_space) = formatted.chars().position(|c| c != ' ') {
actual_number = &formatted[first_non_space..];
leading_spaces = &formatted[0..first_non_space];
}
Ok(leading_spaces.to_owned() + sign_prefix + actual_number)
}
// unsigned-only formats
ConversionType::HexIntLower | ConversionType::HexIntUpper | ConversionType::OctInt => {
(*self as u64).format(spec)
}
_ => Err(PrintfError::WrongType),
}
}
fn as_int(&self) -> Option<i32> {
i32::try_from(*self).ok()
}
}
impl Printf for i32 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
match spec.conversion_type {
// signed integer format
ConversionType::DecInt => (*self as i64).format(spec),
// unsigned-only formats
ConversionType::HexIntLower | ConversionType::HexIntUpper | ConversionType::OctInt => {
(*self as u32).format(spec)
}
_ => Err(PrintfError::WrongType),
}
}
fn as_int(&self) -> Option<i32> {
Some(*self)
}
}
impl Printf for u32 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as u64).format(spec)
}
fn as_int(&self) -> Option<i32> {
i32::try_from(*self).ok()
}
}
impl Printf for i16 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
match spec.conversion_type {
// signed integer format
ConversionType::DecInt => (*self as i64).format(spec),
// unsigned-only formats
ConversionType::HexIntLower | ConversionType::HexIntUpper | ConversionType::OctInt => {
(*self as u16).format(spec)
}
_ => Err(PrintfError::WrongType),
}
}
fn as_int(&self) -> Option<i32> {
Some(*self as i32)
}
}
impl Printf for u16 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as u64).format(spec)
}
fn as_int(&self) -> Option<i32> {
Some(*self as i32)
}
}
impl Printf for i8 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
match spec.conversion_type {
// signed integer format
ConversionType::DecInt => (*self as i64).format(spec),
// unsigned-only formats
ConversionType::HexIntLower | ConversionType::HexIntUpper | ConversionType::OctInt => {
(*self as u8).format(spec)
}
_ => Err(PrintfError::WrongType),
}
}
fn as_int(&self) -> Option<i32> {
Some(*self as i32)
}
}
impl Printf for u8 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as u64).format(spec)
}
fn as_int(&self) -> Option<i32> {
Some(*self as i32)
}
}
impl Printf for usize {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as u64).format(spec)
}
fn as_int(&self) -> Option<i32> {
i32::try_from(*self).ok()
}
}
impl Printf for isize {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as u64).format(spec)
}
fn as_int(&self) -> Option<i32> {
i32::try_from(*self).ok()
}
}
impl Printf for f64 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
let mut prefix = WString::new();
let mut number = WString::new();
// set up the sign
if self.is_sign_negative() {
prefix.push('-');
} else if spec.space_sign {
prefix.push(' ');
} else if spec.force_sign {
prefix.push('+');
}
if self.is_finite() {
let mut use_scientific = false;
let mut exp_symb = 'e';
let mut strip_trailing_0s = false;
let mut abs = self.abs();
let mut exponent = abs.log10().floor() as i32;
let mut precision = match spec.precision {
NumericParam::Literal(p) => p,
_ => {
return Err(PrintfError::Unknown);
}
};
if precision <= 0 {
precision = 0;
}
match spec.conversion_type {
ConversionType::DecFloatLower | ConversionType::DecFloatUpper => {
// default
}
ConversionType::SciFloatLower => {
use_scientific = true;
}
ConversionType::SciFloatUpper => {
use_scientific = true;
exp_symb = 'E';
}
ConversionType::CompactFloatLower | ConversionType::CompactFloatUpper => {
if spec.conversion_type == ConversionType::CompactFloatUpper {
exp_symb = 'E'
}
strip_trailing_0s = true;
if precision == 0 {
precision = 1;
}
// exponent signifies significant digits - we must round now
// to (re)calculate the exponent
let rounding_factor = 10.0_f64.powf((precision - 1 - exponent) as f64);
let rounded_fixed = (abs * rounding_factor).round();
abs = rounded_fixed / rounding_factor;
exponent = abs.log10().floor() as i32;
if exponent < -4 || exponent >= precision {
use_scientific = true;
precision -= 1;
} else {
// precision specifies the number of significant digits
precision -= 1 + exponent;
}
}
_ => {
return Err(PrintfError::WrongType);
}
}
if use_scientific {
let mut normal = abs / 10.0_f64.powf(exponent as f64);
if precision > 0 {
let mut int_part = normal.trunc();
let mut exp_factor = 10.0_f64.powf(precision as f64);
let mut tail = ((normal - int_part) * exp_factor).round() as u64;
while tail >= exp_factor as u64 {
// Overflow, must round
int_part += 1.0;
tail -= exp_factor as u64;
if int_part >= 10.0 {
// keep same precision - which means changing exponent
exponent += 1;
exp_factor /= 10.0;
normal /= 10.0;
int_part = normal.trunc();
tail = ((normal - int_part) * exp_factor).round() as u64;
}
}
let mut rev_tail_str = WString::new();
for _ in 0..precision {
rev_tail_str.push((b'0' + (tail % 10) as u8) as char);
tail /= 10;
}
number.push_str(&int_part.to_string());
number.push('.');
number.extend(rev_tail_str.chars().rev());
if strip_trailing_0s {
while number.ends_with('0') {
number.pop();
}
}
} else {
number.push_str(&format!("{}", normal.round()));
}
number.push(exp_symb);
number.push_str(&format!("{exponent:+03}"));
} else if precision > 0 {
let mut int_part = abs.trunc();
let exp_factor = 10.0_f64.powf(precision as f64);
let mut tail = ((abs - int_part) * exp_factor).round() as u64;
let mut rev_tail_str = WString::new();
if tail >= exp_factor as u64 {
// overflow - we must round up
int_part += 1.0;
tail -= exp_factor as u64;
// no need to change the exponent as we don't have one
// (not scientific notation)
}
for _ in 0..precision {
rev_tail_str.push((b'0' + (tail % 10) as u8) as char);
tail /= 10;
}
number.push_str(&int_part.to_string());
number.push('.');
number.extend(rev_tail_str.chars().rev());
if strip_trailing_0s {
while number.ends_with('0') {
number.pop();
}
}
} else {
number.push_str(&format!("{}", abs.round()));
}
} else {
// not finite
match spec.conversion_type {
ConversionType::DecFloatLower
| ConversionType::SciFloatLower
| ConversionType::CompactFloatLower => {
if self.is_infinite() {
number.push_str("inf")
} else {
number.push_str("nan")
}
}
ConversionType::DecFloatUpper
| ConversionType::SciFloatUpper
| ConversionType::CompactFloatUpper => {
if self.is_infinite() {
number.push_str("INF")
} else {
number.push_str("NAN")
}
}
_ => {
return Err(PrintfError::WrongType);
}
}
}
// Take care of padding
let width: usize = match spec.width {
NumericParam::Literal(w) => w,
_ => {
return Err(PrintfError::Unknown); // should not happen at this point!!
}
}
.try_into()
.unwrap_or_default();
let formatted = if spec.left_adj {
let mut full_num = prefix + &*number;
while full_num.len() < width {
full_num.push(' ');
}
full_num
} else if spec.zero_pad && self.is_finite() {
while prefix.len() + number.len() < width {
prefix.push('0');
}
prefix + &*number
} else {
let mut full_num = prefix + &*number;
while full_num.len() < width {
full_num.insert(0, ' ');
}
full_num
};
Ok(formatted)
}
fn as_int(&self) -> Option<i32> {
None
}
}
impl Printf for f32 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as f64).format(spec)
}
}
impl Printf for &wstr {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
if spec.conversion_type == ConversionType::String {
Ok((*self).to_owned())
} else {
Err(PrintfError::WrongType)
}
}
}
impl Printf for &str {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
if spec.conversion_type == ConversionType::String {
add_padding((*self).into(), spec)
} else {
Err(PrintfError::WrongType)
}
}
}
impl Printf for char {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
if spec.conversion_type == ConversionType::Char {
let mut s = WString::new();
s.push(*self);
Ok(s)
} else {
Err(PrintfError::WrongType)
}
}
}
impl Printf for String {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
self.as_str().format(spec)
}
}
impl Printf for WString {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
self.as_utfstr().format(spec)
}
}
impl Printf for &WString {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
self.as_utfstr().format(spec)
}
}
fn add_padding(mut s: WString, spec: &ConversionSpecifier) -> Result<WString> {
let width: usize = match spec.width {
NumericParam::Literal(w) => w,
_ => {
return Err(PrintfError::Unknown); // should not happen at this point!!
}
}
.try_into()
.unwrap_or_default();
if s.len() < width {
let padding = L!(" ").repeat(width - s.len());
s.insert_utfstr(0, &padding);
};
Ok(s)
}

View File

@ -1,7 +0,0 @@
#[allow(clippy::module_inception)]
mod format;
mod parser;
pub mod printf;
#[cfg(test)]
mod tests;

View File

@ -1,218 +0,0 @@
// Adapted from https://github.com/tjol/sprintf-rs
// License follows:
//
// Copyright (c) 2021 Thomas Jollans
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is furnished
// to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
// OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
use super::printf::{PrintfError, Result};
use crate::wchar::{wstr, WExt, WString};
#[derive(Debug, Clone)]
pub enum FormatElement {
Verbatim(WString),
Format(ConversionSpecifier),
}
/// Parsed printf conversion specifier
#[derive(Debug, Clone, Copy)]
pub struct ConversionSpecifier {
/// flag `#`: use `0x`, etc?
pub alt_form: bool,
/// flag `0`: left-pad with zeros?
pub zero_pad: bool,
/// flag `-`: left-adjust (pad with spaces on the right)
pub left_adj: bool,
/// flag `' '` (space): indicate sign with a space?
pub space_sign: bool,
/// flag `+`: Always show sign? (for signed numbers)
pub force_sign: bool,
/// field width
pub width: NumericParam,
/// floating point field precision
pub precision: NumericParam,
/// data type
pub conversion_type: ConversionType,
}
/// Width / precision parameter
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumericParam {
/// The literal width
Literal(i32),
/// Get the width from the previous argument
///
/// This should never be passed to [Printf::format()][super::format::Printf::format()].
FromArgument,
}
/// Printf data type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConversionType {
/// `d`, `i`, or `u`
DecInt,
/// `o`
OctInt,
/// `x` or `p`
HexIntLower,
/// `X`
HexIntUpper,
/// `e`
SciFloatLower,
/// `E`
SciFloatUpper,
/// `f`
DecFloatLower,
/// `F`
DecFloatUpper,
/// `g`
CompactFloatLower,
/// `G`
CompactFloatUpper,
/// `c`
Char,
/// `s`
String,
/// `%`
PercentSign,
}
pub(crate) fn parse_format_string(fmt: &wstr) -> Result<Vec<FormatElement>> {
// find the first %
let mut res = Vec::new();
let parts: Vec<&wstr> = match fmt.find_char('%') {
Some(i) => vec![&fmt[..i], &fmt[(i + 1)..]],
None => vec![fmt],
};
if !parts[0].is_empty() {
res.push(FormatElement::Verbatim(parts[0].to_owned()));
}
if parts.len() > 1 {
let (spec, rest) = take_conversion_specifier(parts[1])?;
res.push(FormatElement::Format(spec));
res.append(&mut parse_format_string(rest)?);
}
Ok(res)
}
fn take_conversion_specifier(s: &wstr) -> Result<(ConversionSpecifier, &wstr)> {
let mut spec = ConversionSpecifier {
alt_form: false,
zero_pad: false,
left_adj: false,
space_sign: false,
force_sign: false,
width: NumericParam::Literal(0),
precision: NumericParam::Literal(6),
// ignore length modifier
conversion_type: ConversionType::DecInt,
};
let mut s = s;
// parse flags
loop {
match s.chars().next() {
Some('#') => {
spec.alt_form = true;
}
Some('0') => {
spec.zero_pad = true;
}
Some('-') => {
spec.left_adj = true;
}
Some(' ') => {
spec.space_sign = true;
}
Some('+') => {
spec.force_sign = true;
}
_ => {
break;
}
}
s = &s[1..];
}
// parse width
let (w, mut s) = take_numeric_param(s);
spec.width = w;
// parse precision
if matches!(s.chars().next(), Some('.')) {
s = &s[1..];
let (p, s2) = take_numeric_param(s);
spec.precision = p;
s = s2;
}
// check length specifier
for len_spec in ["hh", "h", "l", "ll", "q", "L", "j", "z", "Z", "t"] {
if s.starts_with(len_spec) {
s = &s[len_spec.len()..];
break; // only allow one length specifier
}
}
// parse conversion type
spec.conversion_type = match s.chars().next() {
Some('i') | Some('d') | Some('u') => ConversionType::DecInt,
Some('o') => ConversionType::OctInt,
Some('x') => ConversionType::HexIntLower,
Some('X') => ConversionType::HexIntUpper,
Some('e') => ConversionType::SciFloatLower,
Some('E') => ConversionType::SciFloatUpper,
Some('f') => ConversionType::DecFloatLower,
Some('F') => ConversionType::DecFloatUpper,
Some('g') => ConversionType::CompactFloatLower,
Some('G') => ConversionType::CompactFloatUpper,
Some('c') | Some('C') => ConversionType::Char,
Some('s') | Some('S') => ConversionType::String,
Some('p') => {
spec.alt_form = true;
ConversionType::HexIntLower
}
Some('%') => ConversionType::PercentSign,
_ => {
return Err(PrintfError::ParseError);
}
};
Ok((spec, &s[1..]))
}
fn take_numeric_param(s: &wstr) -> (NumericParam, &wstr) {
match s.chars().next() {
Some('*') => (NumericParam::FromArgument, &s[1..]),
Some(digit) if ('1'..='9').contains(&digit) => {
let mut s = s;
let mut w = 0;
loop {
match s.chars().next() {
Some(digit) if ('0'..='9').contains(&digit) => {
w = 10 * w + (digit as i32 - '0' as i32);
}
_ => {
break;
}
}
s = &s[1..];
}
(NumericParam::Literal(w), s)
}
_ => (NumericParam::Literal(0), s),
}
}

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