Compare commits
No commits in common. "master" and "Integration_3.6.0" have entirely different histories.
master
...
Integratio
30
.cirrus.yml
30
.cirrus.yml
|
|
@ -51,8 +51,7 @@ 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'
|
||||
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'
|
||||
|
||||
linux_arm_task:
|
||||
matrix:
|
||||
|
|
@ -75,24 +74,21 @@ linux_arm_task:
|
|||
- file ./fish
|
||||
- ninja fish_run_tests
|
||||
|
||||
# CI task disabled during RIIR transition
|
||||
only_if: false && $CIRRUS_REPO_OWNER == 'fish-shell'
|
||||
only_if: $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 +99,8 @@ 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'
|
||||
- sudo -u fish-user -s ninja -j 6 fish fish_tests
|
||||
- sudo -u fish-user -s ninja fish_run_tests
|
||||
|
||||
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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,9 +78,7 @@ 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
|
||||
|
|
@ -121,50 +95,38 @@ 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:
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,74 +1,3 @@
|
|||
fish 3.7.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)
|
||||
=====================================
|
||||
|
||||
|
|
|
|||
|
|
@ -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/abbr.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/ast.cpp src/abbrs.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/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})
|
||||
|
||||
|
|
|
|||
|
|
@ -18,13 +18,6 @@ Completion scripts are the most common contribution to fish, and they are very w
|
|||
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``,
|
||||
|
|
@ -34,11 +27,8 @@ Completion scripts should
|
|||
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.
|
||||
Put your completion script into share/completions/name-of-command.fish.
|
||||
|
||||
If you want to add tests, you probably want to add a littlecheck test. See below for details.
|
||||
|
||||
|
|
@ -420,8 +410,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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,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 fish’s design philosophy, see the `design
|
||||
document <https://fishshell.com/docs/current/design.html>`__.
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
|
|
@ -148,7 +149,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)
|
||||
|
|
|
|||
|
|
@ -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\//"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -32,23 +32,9 @@ An abbreviation may match a literal word, or it may match a pattern given by a r
|
|||
|
||||
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.
|
||||
|
||||
.. 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
|
||||
Abbreviations may be added to :ref:`config.fish <configuration>`.
|
||||
|
||||
::
|
||||
|
||||
> abbr
|
||||
abbr -a -- foo bar # imported from a universal variable, see `help abbr`
|
||||
|
||||
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::
|
||||
|
||||
> abbr >> ~/.config/fish/config.fish
|
||||
> abbr --erase (abbr --list)
|
||||
|
||||
|
||||
"add" subcommand
|
||||
--------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
--------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ 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.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
::
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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[@]}"``::
|
||||
|
|
|
|||
|
|
@ -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
|
||||
=============
|
||||
|
||||
|
|
|
|||
|
|
@ -235,21 +235,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:
|
||||
|
||||
|
|
@ -363,8 +349,6 @@ To enable emacs mode, use ``fish_default_key_bindings``. This is also the defaul
|
|||
|
||||
- :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:`Alt`\ +\ :kbd:`C` capitalizes the current word.
|
||||
|
||||
- :kbd:`Alt`\ +\ :kbd:`U` makes the current word uppercase.
|
||||
|
|
@ -420,9 +404,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
|
||||
|
|
@ -454,10 +435,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 +444,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,10 +451,6 @@ 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
|
||||
|
|
@ -531,18 +503,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:
|
||||
|
|
|
|||
|
|
@ -69,32 +69,30 @@ Here we define some of the terms used on this page and throughout the rest of th
|
|||
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
|
||||
|
|
@ -243,7 +241,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::
|
||||
|
||||
|
|
@ -379,81 +377,32 @@ 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
|
||||
|
|
@ -473,7 +422,7 @@ and::
|
|||
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.
|
||||
These do essentially the same thing, but the former takes 2 seconds longer because the ``sleep`` always needs to run. So, in cases like these, the ordering is quite important for performance.
|
||||
|
||||
Or you can have a case where it is necessary to stop early::
|
||||
|
||||
|
|
@ -481,20 +430,7 @@ Or you can have a case where it is necessary to stop early::
|
|||
|
||||
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:
|
||||
|
||||
|
|
@ -693,49 +629,26 @@ 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)``, fish first runs ``innercommand``, and then uses each line of its output as a separate argument to ``outercommand``, which will then be executed. Unlike other shells, the value of ``$IFS`` is not used [#]_, fish splits on newlines.
|
||||
|
||||
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, but trailing empty lines are still removed.
|
||||
|
||||
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 some lines of the output, refer to :ref:`index range expansion <expand-index-range>`.
|
||||
|
||||
Examples::
|
||||
|
||||
|
|
@ -753,6 +666,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
|
||||
|
|
@ -877,10 +791,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 +814,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.
|
||||
|
||||
|
|
@ -1030,8 +944,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.
|
||||
|
||||
|
|
@ -1099,33 +1013,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 +1037,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 +1056,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,6 +1067,30 @@ 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
|
||||
|
|
@ -1257,7 +1175,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>`.
|
||||
|
||||
|
|
@ -1320,14 +1238,10 @@ That covers the positional arguments, but commandline tools often get various op
|
|||
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::
|
||||
|
||||
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.
|
||||
# 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
|
||||
# exit if argparse failed because it found an option it didn't recognize - it will print an error
|
||||
or return
|
||||
|
||||
# If -h or --help is given, we print a little help text and return
|
||||
|
|
@ -1409,16 +1323,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.
|
||||
|
|
@ -1766,8 +1670,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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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")));
|
||||
})
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
@ -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 _) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 _)
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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'";
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(®ex_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(®ex_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()));
|
||||
});
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
mod fd_monitor;
|
||||
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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(¤t);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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")));
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
#[allow(clippy::module_inception)]
|
||||
mod format;
|
||||
mod parser;
|
||||
pub mod printf;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,126 +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.
|
||||
|
||||
pub use super::format::Printf;
|
||||
use super::parser::{parse_format_string, ConversionType, FormatElement, NumericParam};
|
||||
use crate::wchar::{wstr, WString};
|
||||
|
||||
/// Error type
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PrintfError {
|
||||
/// Error parsing the format string
|
||||
ParseError,
|
||||
/// Incorrect type passed as an argument
|
||||
WrongType,
|
||||
/// Too many arguments passed
|
||||
TooManyArgs,
|
||||
/// Too few arguments passed
|
||||
NotEnoughArgs,
|
||||
/// Other error (should never happen)
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, PrintfError>;
|
||||
|
||||
/// Format a string. (Roughly equivalent to `vsnprintf` or `vasprintf` in C)
|
||||
///
|
||||
/// Takes a printf-style format string `format` and a slice of dynamically
|
||||
/// typed arguments, `args`.
|
||||
///
|
||||
/// use sprintf::{vsprintf, Printf};
|
||||
/// let n = 16;
|
||||
/// let args: Vec<&dyn Printf> = vec![&n];
|
||||
/// let s = vsprintf("%#06x", &args).unwrap();
|
||||
/// assert_eq!(s, "0x0010");
|
||||
///
|
||||
/// See also: [sprintf]
|
||||
pub fn vsprintf(format: &wstr, args: &[&dyn Printf]) -> Result<WString> {
|
||||
vsprintfp(&parse_format_string(format)?, args)
|
||||
}
|
||||
|
||||
fn vsprintfp(format: &[FormatElement], args: &[&dyn Printf]) -> Result<WString> {
|
||||
let mut res = WString::new();
|
||||
|
||||
let mut args = args;
|
||||
let mut pop_arg = || {
|
||||
if args.is_empty() {
|
||||
Err(PrintfError::NotEnoughArgs)
|
||||
} else {
|
||||
let a = args[0];
|
||||
args = &args[1..];
|
||||
Ok(a)
|
||||
}
|
||||
};
|
||||
|
||||
for elem in format {
|
||||
match elem {
|
||||
FormatElement::Verbatim(s) => {
|
||||
res.push_utfstr(s);
|
||||
}
|
||||
FormatElement::Format(spec) => {
|
||||
if spec.conversion_type == ConversionType::PercentSign {
|
||||
res.push('%');
|
||||
} else {
|
||||
let mut completed_spec = *spec;
|
||||
if spec.width == NumericParam::FromArgument {
|
||||
completed_spec.width = NumericParam::Literal(
|
||||
pop_arg()?.as_int().ok_or(PrintfError::WrongType)?,
|
||||
)
|
||||
}
|
||||
if spec.precision == NumericParam::FromArgument {
|
||||
completed_spec.precision = NumericParam::Literal(
|
||||
pop_arg()?.as_int().ok_or(PrintfError::WrongType)?,
|
||||
)
|
||||
}
|
||||
res.push_utfstr(&pop_arg()?.format(&completed_spec)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if args.is_empty() {
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(PrintfError::TooManyArgs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a string. (Roughly equivalent to `snprintf` or `asprintf` in C)
|
||||
///
|
||||
/// Takes a printf-style format string `format` and a variable number of
|
||||
/// additional arguments.
|
||||
///
|
||||
/// use sprintf::sprintf;
|
||||
/// let s = sprintf!("%s = %*d", "forty-two", 4, 42);
|
||||
/// assert_eq!(s, "forty-two = 42");
|
||||
///
|
||||
/// Wrapper around [vsprintf].
|
||||
macro_rules! sprintf {
|
||||
(
|
||||
$fmt:expr, // format string
|
||||
$($arg:expr),* // arguments
|
||||
$(,)? // optional trailing comma
|
||||
) => {
|
||||
crate::wutil::format::printf::vsprintf($fmt, &[$( &($arg) as &dyn crate::wutil::format::printf::Printf),* ][..]).expect("Invalid format string and/or arguments")
|
||||
};
|
||||
}
|
||||
pub(crate) use sprintf;
|
||||
|
|
@ -1,124 +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::{sprintf, Printf};
|
||||
use crate::wchar::{widestrs, WString, L};
|
||||
|
||||
fn check_fmt<T: Printf>(nfmt: &str, arg: T, expected: &str) {
|
||||
let fmt: WString = nfmt.into();
|
||||
let our_result = sprintf!(&fmt, arg);
|
||||
assert_eq!(our_result, expected);
|
||||
}
|
||||
|
||||
fn check_fmt_2<T: Printf, T2: Printf>(nfmt: &str, arg: T, arg2: T2, expected: &str) {
|
||||
let fmt: WString = nfmt.into();
|
||||
let our_result = sprintf!(&fmt, arg, arg2);
|
||||
assert_eq!(our_result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int() {
|
||||
check_fmt("%d", 12, "12");
|
||||
check_fmt("~%d~", 148, "~148~");
|
||||
check_fmt("00%dxx", -91232, "00-91232xx");
|
||||
check_fmt("%x", -9232, "ffffdbf0");
|
||||
check_fmt("%X", 432, "1B0");
|
||||
check_fmt("%09X", 432, "0000001B0");
|
||||
check_fmt("%9X", 432, " 1B0");
|
||||
check_fmt("%+9X", 492, " 1EC");
|
||||
check_fmt("% #9x", 4589, " 0x11ed");
|
||||
check_fmt("%2o", 4, " 4");
|
||||
check_fmt("% 12d", -4, " -4");
|
||||
check_fmt("% 12d", 48, " 48");
|
||||
check_fmt("%ld", -4_i64, "-4");
|
||||
check_fmt("%lX", -4_i64, "FFFFFFFFFFFFFFFC");
|
||||
check_fmt("%ld", 48_i64, "48");
|
||||
check_fmt("%-8hd", -12_i16, "-12 ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float() {
|
||||
check_fmt("%f", -46.38, "-46.380000");
|
||||
check_fmt("%012.3f", 1.2, "00000001.200");
|
||||
check_fmt("%012.3e", 1.7, "0001.700e+00");
|
||||
check_fmt("%e", 1e300, "1.000000e+300");
|
||||
check_fmt("%012.3g%%!", 2.6, "0000000002.6%!");
|
||||
check_fmt("%012.5G", -2.69, "-00000002.69");
|
||||
check_fmt("%+7.4f", 42.785, "+42.7850");
|
||||
check_fmt("{}% 7.4E", 493.12, "{} 4.9312E+02");
|
||||
check_fmt("% 7.4E", -120.3, "-1.2030E+02");
|
||||
check_fmt("%-10F", f64::INFINITY, "INF ");
|
||||
check_fmt("%+010F", f64::INFINITY, " +INF");
|
||||
check_fmt("% f", f64::NAN, " nan");
|
||||
check_fmt("%+f", f64::NAN, "+nan");
|
||||
check_fmt("%.1f", 999.99, "1000.0");
|
||||
check_fmt("%.1f", 9.99, "10.0");
|
||||
check_fmt("%.1e", 9.99, "1.0e+01");
|
||||
check_fmt("%.2f", 9.99, "9.99");
|
||||
check_fmt("%.2e", 9.99, "9.99e+00");
|
||||
check_fmt("%.3f", 9.99, "9.990");
|
||||
check_fmt("%.3e", 9.99, "9.990e+00");
|
||||
check_fmt("%.1g", 9.99, "1e+01");
|
||||
check_fmt("%.1G", 9.99, "1E+01");
|
||||
check_fmt("%.1f", 2.99, "3.0");
|
||||
check_fmt("%.1e", 2.99, "3.0e+00");
|
||||
check_fmt("%.1g", 2.99, "3");
|
||||
check_fmt("%.1f", 2.599, "2.6");
|
||||
check_fmt("%.1e", 2.599, "2.6e+00");
|
||||
check_fmt("%.1g", 2.599, "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_str() {
|
||||
check_fmt(
|
||||
"test %% with string: %s yay\n",
|
||||
"FOO",
|
||||
"test % with string: FOO yay\n",
|
||||
);
|
||||
check_fmt("test char %c", '~', "test char ~");
|
||||
check_fmt_2("%*ls", 5, "^", " ^");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[widestrs]
|
||||
fn test_str_concat() {
|
||||
assert_eq!(sprintf!("%s-%ls"L, "abc", "def"L), "abc-def"L);
|
||||
assert_eq!(sprintf!("%s-%ls"L, "abc", "def"L), "abc-def"L);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_bad_format() {
|
||||
sprintf!(L!("%s"), 123);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_missing_arg() {
|
||||
sprintf!(L!("%s-%s"), "abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_too_many_args() {
|
||||
sprintf!(L!("%d"), 1, 2, 3);
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
use crate::ffi;
|
||||
use crate::wchar::wstr;
|
||||
use crate::wchar_ffi::{wchar_t, wcslen};
|
||||
|
||||
/// Support for wgettext.
|
||||
|
||||
/// Implementation detail for wgettext!.
|
||||
pub fn wgettext_impl_do_not_use_directly(text: &[wchar_t]) -> &'static wstr {
|
||||
assert_eq!(text.last(), Some(&0), "should be nul-terminated");
|
||||
let res: *const wchar_t = ffi::wgettext_ptr(text.as_ptr());
|
||||
let slice = unsafe { std::slice::from_raw_parts(res as *const u32, wcslen(res)) };
|
||||
wstr::from_slice(slice).expect("Invalid UTF-32")
|
||||
}
|
||||
|
||||
/// Get a (possibly translated) string from a string literal.
|
||||
/// This returns a &'static wstr.
|
||||
macro_rules! wgettext {
|
||||
($string:expr) => {
|
||||
crate::wutil::gettext::wgettext_impl_do_not_use_directly(
|
||||
crate::wchar_ffi::u32cstr!($string).as_slice_with_nul(),
|
||||
)
|
||||
};
|
||||
}
|
||||
pub(crate) use wgettext;
|
||||
|
||||
/// Like wgettext, but applies a sprintf format string.
|
||||
/// The result is a WString.
|
||||
macro_rules! wgettext_fmt {
|
||||
(
|
||||
$string:expr // format string
|
||||
$(, $args:expr)* // list of expressions
|
||||
$(,)? // optional trailing comma
|
||||
) => {
|
||||
crate::wutil::sprintf!(&crate::wutil::wgettext!($string), $($args),*)
|
||||
};
|
||||
}
|
||||
pub(crate) use wgettext_fmt;
|
||||
|
||||
use crate::ffi_tests::add_test;
|
||||
add_test!("test_untranslated", || {
|
||||
let s: &'static wstr = wgettext!("abc");
|
||||
assert_eq!(s, "abc");
|
||||
let s2: &'static wstr = wgettext!("static");
|
||||
assert_eq!(s2, "static");
|
||||
});
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
pub mod format;
|
||||
pub mod gettext;
|
||||
mod normalize_path;
|
||||
mod wcstoi;
|
||||
mod wrealpath;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use crate::wchar::{wstr, WString};
|
||||
pub(crate) use format::printf::sprintf;
|
||||
pub(crate) use gettext::{wgettext, wgettext_fmt};
|
||||
pub use normalize_path::*;
|
||||
pub use wcstoi::*;
|
||||
pub use wrealpath::*;
|
||||
|
||||
/// Port of the wide-string wperror from `src/wutil.cpp` but for rust `&str`.
|
||||
pub fn perror(s: &str) {
|
||||
let e = errno::errno().0;
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
if !s.is_empty() {
|
||||
let _ = write!(stderr, "{s}: ");
|
||||
}
|
||||
let slice = unsafe {
|
||||
let msg = libc::strerror(e) as *const u8;
|
||||
let len = libc::strlen(msg as *const _);
|
||||
std::slice::from_raw_parts(msg, len)
|
||||
};
|
||||
let _ = stderr.write_all(slice);
|
||||
let _ = stderr.write_all(b"\n");
|
||||
}
|
||||
|
||||
/// Joins strings with a separator.
|
||||
pub fn join_strings(strs: &[&wstr], sep: char) -> WString {
|
||||
if strs.is_empty() {
|
||||
return WString::new();
|
||||
}
|
||||
let capacity = strs.iter().fold(0, |acc, s| acc + s.len()) + strs.len() - 1;
|
||||
let mut result = WString::with_capacity(capacity);
|
||||
for (i, s) in strs.iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push(sep);
|
||||
}
|
||||
result.push_utfstr(s);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_strings() {
|
||||
use crate::wchar::L;
|
||||
assert_eq!(join_strings(&[], '/'), "");
|
||||
assert_eq!(join_strings(&[L!("foo")], '/'), "foo");
|
||||
assert_eq!(
|
||||
join_strings(&[L!("foo"), L!("bar"), L!("baz")], '/'),
|
||||
"foo/bar/baz"
|
||||
);
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
use crate::wchar::{wstr, WString, L};
|
||||
use crate::wutil::join_strings;
|
||||
|
||||
/// Given an input path, "normalize" it:
|
||||
/// 1. Collapse multiple /s into a single /, except maybe at the beginning.
|
||||
/// 2. .. goes up a level.
|
||||
/// 3. Remove /./ in the middle.
|
||||
pub fn normalize_path(path: &wstr, allow_leading_double_slashes: bool) -> WString {
|
||||
// Count the leading slashes.
|
||||
let sep = '/';
|
||||
let mut leading_slashes: usize = 0;
|
||||
for c in path.chars() {
|
||||
if c != sep {
|
||||
break;
|
||||
}
|
||||
leading_slashes += 1;
|
||||
}
|
||||
|
||||
let comps = path
|
||||
.as_char_slice()
|
||||
.split(|&c| c == sep)
|
||||
.map(wstr::from_char_slice)
|
||||
.collect::<Vec<_>>();
|
||||
let mut new_comps = Vec::new();
|
||||
for comp in comps {
|
||||
if comp.is_empty() || comp == "." {
|
||||
continue;
|
||||
} else if comp != ".." {
|
||||
new_comps.push(comp);
|
||||
} else if !new_comps.is_empty() && new_comps.last().unwrap() != ".." {
|
||||
// '..' with a real path component, drop that path component.
|
||||
new_comps.pop();
|
||||
} else if leading_slashes == 0 {
|
||||
// We underflowed the .. and are a relative (not absolute) path.
|
||||
new_comps.push(L!(".."));
|
||||
}
|
||||
}
|
||||
let mut result = join_strings(&new_comps, sep);
|
||||
// If we don't allow leading double slashes, collapse them to 1 if there are any.
|
||||
let mut numslashes = if leading_slashes > 0 { 1 } else { 0 };
|
||||
// If we do, prepend one or two leading slashes.
|
||||
// Yes, three+ slashes are collapsed to one. (!)
|
||||
if allow_leading_double_slashes && leading_slashes == 2 {
|
||||
numslashes = 2;
|
||||
}
|
||||
for _ in 0..numslashes {
|
||||
result.insert(0, sep);
|
||||
}
|
||||
// Ensure ./ normalizes to . and not empty.
|
||||
if result.is_empty() {
|
||||
result.push('.');
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_path() {
|
||||
fn norm_path(path: &wstr) -> WString {
|
||||
normalize_path(path, true)
|
||||
}
|
||||
assert_eq!(norm_path(L!("")), ".");
|
||||
assert_eq!(norm_path(L!("..")), "..");
|
||||
assert_eq!(norm_path(L!("./")), ".");
|
||||
assert_eq!(norm_path(L!("./.")), ".");
|
||||
assert_eq!(norm_path(L!("/")), "/");
|
||||
assert_eq!(norm_path(L!("//")), "//");
|
||||
assert_eq!(norm_path(L!("///")), "/");
|
||||
assert_eq!(norm_path(L!("////")), "/");
|
||||
assert_eq!(norm_path(L!("/.///")), "/");
|
||||
assert_eq!(norm_path(L!(".//")), ".");
|
||||
assert_eq!(norm_path(L!("/.//../")), "/");
|
||||
assert_eq!(norm_path(L!("////abc")), "/abc");
|
||||
assert_eq!(norm_path(L!("/abc")), "/abc");
|
||||
assert_eq!(norm_path(L!("/abc/")), "/abc");
|
||||
assert_eq!(norm_path(L!("/abc/..def/")), "/abc/..def");
|
||||
assert_eq!(norm_path(L!("//abc/../def/")), "//def");
|
||||
assert_eq!(norm_path(L!("abc/../abc/../abc/../abc")), "abc");
|
||||
assert_eq!(norm_path(L!("../../")), "../..");
|
||||
assert_eq!(norm_path(L!("foo/./bar")), "foo/bar");
|
||||
assert_eq!(norm_path(L!("foo/../")), ".");
|
||||
assert_eq!(norm_path(L!("foo/../foo")), "foo");
|
||||
assert_eq!(norm_path(L!("foo/../foo/")), "foo");
|
||||
assert_eq!(norm_path(L!("foo/././bar/.././baz")), "foo/baz");
|
||||
}
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
use num_traits::{NumCast, PrimInt};
|
||||
use std::iter::Peekable;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
Overflow,
|
||||
Empty,
|
||||
InvalidDigit,
|
||||
CharsLeft,
|
||||
}
|
||||
|
||||
struct ParseResult {
|
||||
result: u64,
|
||||
negative: bool,
|
||||
consumed_all: bool,
|
||||
}
|
||||
|
||||
/// Helper to get the current char, or \0.
|
||||
fn current<Chars>(chars: &mut Peekable<Chars>) -> char
|
||||
where
|
||||
Chars: Iterator<Item = char>,
|
||||
{
|
||||
match chars.peek() {
|
||||
Some(c) => *c,
|
||||
None => '\0',
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the given \p src as an integer.
|
||||
/// If mradix is not None, it is used as the radix; otherwise the radix is inferred:
|
||||
/// - Leading 0x or 0X means 16.
|
||||
/// - Leading 0 means 8.
|
||||
/// - Otherwise 10.
|
||||
/// The parse result contains the number as a u64, and whether it was negative.
|
||||
fn fish_parse_radix<Chars>(ichars: Chars, mradix: Option<u32>) -> Result<ParseResult, Error>
|
||||
where
|
||||
Chars: Iterator<Item = char>,
|
||||
{
|
||||
if let Some(r) = mradix {
|
||||
assert!((2..=36).contains(&r), "fish_parse_radix: invalid radix {r}");
|
||||
}
|
||||
let chars = &mut ichars.peekable();
|
||||
|
||||
// Skip leading whitespace.
|
||||
while current(chars).is_whitespace() {
|
||||
chars.next();
|
||||
}
|
||||
|
||||
if chars.peek().is_none() {
|
||||
return Err(Error::Empty);
|
||||
}
|
||||
|
||||
// Consume leading +/-.
|
||||
let mut negative;
|
||||
match current(chars) {
|
||||
'-' | '+' => {
|
||||
negative = current(chars) == '-';
|
||||
chars.next();
|
||||
}
|
||||
_ => negative = false,
|
||||
}
|
||||
|
||||
// Determine the radix.
|
||||
let radix = if let Some(radix) = mradix {
|
||||
radix
|
||||
} else if current(chars) == '0' {
|
||||
chars.next();
|
||||
match current(chars) {
|
||||
'x' | 'X' => {
|
||||
chars.next();
|
||||
16
|
||||
}
|
||||
c if ('0'..='9').contains(&c) => 8,
|
||||
_ => {
|
||||
// Just a 0.
|
||||
return Ok(ParseResult {
|
||||
result: 0,
|
||||
negative: false,
|
||||
consumed_all: chars.peek().is_none(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
10
|
||||
};
|
||||
|
||||
// Compute as u64.
|
||||
let mut consumed1 = false;
|
||||
let mut result: u64 = 0;
|
||||
while let Some(digit) = current(chars).to_digit(radix) {
|
||||
result = result
|
||||
.checked_mul(radix as u64)
|
||||
.and_then(|r| r.checked_add(digit as u64))
|
||||
.ok_or(Error::Overflow)?;
|
||||
chars.next();
|
||||
consumed1 = true;
|
||||
}
|
||||
|
||||
// Did we consume at least one char?
|
||||
if !consumed1 {
|
||||
return Err(Error::InvalidDigit);
|
||||
}
|
||||
|
||||
// Do not return -0.
|
||||
if result == 0 {
|
||||
negative = false;
|
||||
}
|
||||
let consumed_all = chars.peek().is_none();
|
||||
Ok(ParseResult {
|
||||
result,
|
||||
negative,
|
||||
consumed_all,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse some iterator over Chars into some Integer type, optionally with a radix.
|
||||
fn fish_wcstoi_impl<Int, Chars>(
|
||||
src: Chars,
|
||||
mradix: Option<u32>,
|
||||
consume_all: bool,
|
||||
) -> Result<Int, Error>
|
||||
where
|
||||
Chars: Iterator<Item = char>,
|
||||
Int: PrimInt,
|
||||
{
|
||||
let bits = Int::zero().count_zeros();
|
||||
assert!(bits <= 64, "fish_wcstoi: Int must be <= 64 bits");
|
||||
let signed = Int::min_value() < Int::zero();
|
||||
|
||||
let ParseResult {
|
||||
result,
|
||||
negative,
|
||||
consumed_all,
|
||||
..
|
||||
} = fish_parse_radix(src, mradix)?;
|
||||
|
||||
if !signed && negative {
|
||||
Err(Error::InvalidDigit)
|
||||
} else if consume_all && !consumed_all {
|
||||
Err(Error::CharsLeft)
|
||||
} else if !signed || !negative {
|
||||
match Int::from(result) {
|
||||
Some(r) => Ok(r),
|
||||
None => Err(Error::Overflow),
|
||||
}
|
||||
} else {
|
||||
assert!(signed && negative);
|
||||
// Signed type, so convert to s64.
|
||||
// Careful of the most negative value.
|
||||
if bits == 64 && result == 1 << 63 {
|
||||
return Ok(Int::min_value());
|
||||
}
|
||||
<i64 as NumCast>::from(result)
|
||||
.and_then(|r| r.checked_neg())
|
||||
.and_then(|r| Int::from(r))
|
||||
.ok_or(Error::Overflow)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the given wide string to an integer.
|
||||
/// The semantics here match wcstol():
|
||||
/// - Leading whitespace is skipped.
|
||||
/// - 0 means octal, 0x means hex
|
||||
/// - Leading + is supported.
|
||||
pub fn fish_wcstoi<Int, Chars>(src: Chars) -> Result<Int, Error>
|
||||
where
|
||||
Chars: Iterator<Item = char>,
|
||||
Int: PrimInt,
|
||||
{
|
||||
fish_wcstoi_impl(src, None, false)
|
||||
}
|
||||
|
||||
/// Convert the given wide string to an integer using the given radix.
|
||||
/// Leading whitespace is skipped.
|
||||
pub fn fish_wcstoi_radix<Int, Chars>(src: Chars, radix: u32) -> Result<Int, Error>
|
||||
where
|
||||
Chars: Iterator<Item = char>,
|
||||
Int: PrimInt,
|
||||
{
|
||||
fish_wcstoi_impl(src, Some(radix), false)
|
||||
}
|
||||
|
||||
pub fn fish_wcstoi_radix_all<Int, Chars>(
|
||||
src: Chars,
|
||||
radix: Option<u32>,
|
||||
consume_all: bool,
|
||||
) -> Result<Int, Error>
|
||||
where
|
||||
Chars: Iterator<Item = char>,
|
||||
Int: PrimInt,
|
||||
{
|
||||
fish_wcstoi_impl(src, radix, consume_all)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_min_max<Int: PrimInt + std::fmt::Display + std::fmt::Debug>(min: Int, max: Int) {
|
||||
assert_eq!(fish_wcstoi(min.to_string().chars()), Ok(min));
|
||||
assert_eq!(fish_wcstoi(max.to_string().chars()), Ok(max));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tests() {
|
||||
let run1 = |s: &str| -> Result<i32, Error> { fish_wcstoi(s.chars()) };
|
||||
let run1_rad =
|
||||
|s: &str, radix: u32| -> Result<i32, Error> { fish_wcstoi_radix(s.chars(), radix) };
|
||||
assert_eq!(run1(""), Err(Error::Empty));
|
||||
assert_eq!(run1(" \n "), Err(Error::Empty));
|
||||
assert_eq!(run1("0"), Ok(0));
|
||||
assert_eq!(run1("-0"), Ok(0));
|
||||
assert_eq!(run1("+0"), Ok(0));
|
||||
assert_eq!(run1("+-0"), Err(Error::InvalidDigit));
|
||||
assert_eq!(run1("-+0"), Err(Error::InvalidDigit));
|
||||
assert_eq!(run1("123"), Ok(123));
|
||||
assert_eq!(run1("+123"), Ok(123));
|
||||
assert_eq!(run1("-123"), Ok(-123));
|
||||
assert_eq!(run1("123"), Ok(123));
|
||||
assert_eq!(run1("+0x123"), Ok(291));
|
||||
assert_eq!(run1("-0x123"), Ok(-291));
|
||||
assert_eq!(run1("+0X123"), Ok(291));
|
||||
assert_eq!(run1("-0X123"), Ok(-291));
|
||||
assert_eq!(run1("+0123"), Ok(83));
|
||||
assert_eq!(run1("-0123"), Ok(-83));
|
||||
assert_eq!(run1(" 345 "), Ok(345));
|
||||
assert_eq!(run1(" -345 "), Ok(-345));
|
||||
assert_eq!(run1(" x345"), Err(Error::InvalidDigit));
|
||||
assert_eq!(run1("456x"), Ok(456));
|
||||
assert_eq!(run1("456 x"), Ok(456));
|
||||
assert_eq!(run1("99999999999999999999999"), Err(Error::Overflow));
|
||||
assert_eq!(run1("-99999999999999999999999"), Err(Error::Overflow));
|
||||
// This is subtle. "567" in base 8 is "375" in base 10. The final "8" is not converted.
|
||||
assert_eq!(run1_rad("5678", 8), Ok(375));
|
||||
|
||||
test_min_max(std::i8::MIN, std::i8::MAX);
|
||||
test_min_max(std::i16::MIN, std::i16::MAX);
|
||||
test_min_max(std::i32::MIN, std::i32::MAX);
|
||||
test_min_max(std::i64::MIN, std::i64::MAX);
|
||||
test_min_max(std::u8::MIN, std::u8::MAX);
|
||||
test_min_max(std::u16::MIN, std::u16::MAX);
|
||||
test_min_max(std::u32::MIN, std::u32::MAX);
|
||||
test_min_max(std::u64::MIN, std::u64::MAX);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
use std::{
|
||||
ffi::OsStr,
|
||||
fs::canonicalize,
|
||||
os::unix::prelude::{OsStrExt, OsStringExt},
|
||||
};
|
||||
|
||||
use cxx::let_cxx_string;
|
||||
|
||||
use crate::{
|
||||
ffi::{str2wcstring, wcs2string},
|
||||
wchar::{wstr, WString},
|
||||
wchar_ffi::{WCharFromFFI, WCharToFFI},
|
||||
};
|
||||
|
||||
/// Wide character realpath. The last path component does not need to be valid. If an error occurs,
|
||||
/// `wrealpath()` returns `None`
|
||||
pub fn wrealpath(pathname: &wstr) -> Option<WString> {
|
||||
if pathname.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut narrow_path: Vec<u8> = wcs2string(&pathname.to_ffi()).from_ffi();
|
||||
|
||||
// Strip trailing slashes. This is treats "/a//" as equivalent to "/a" if /a is a non-directory.
|
||||
while narrow_path.len() > 1 && narrow_path[narrow_path.len() - 1] == b'/' {
|
||||
narrow_path.pop();
|
||||
}
|
||||
|
||||
// `from_bytes` is Unix specific but there isn't really any other way to do this
|
||||
// since `libc::realpath` is also Unix specific. I also don't think we support Windows
|
||||
// outside of WSL + Cygwin (which should be fairly Unix-like anyways)
|
||||
let narrow_res = canonicalize(OsStr::from_bytes(&narrow_path));
|
||||
|
||||
let real_path = if let Ok(result) = narrow_res {
|
||||
result.into_os_string().into_vec()
|
||||
} else {
|
||||
// Check if everything up to the last path component is valid.
|
||||
let pathsep_idx = narrow_path.iter().rposition(|&c| c == b'/');
|
||||
|
||||
if pathsep_idx == Some(0) {
|
||||
// If the only pathsep is the first character then it's an absolute path with a
|
||||
// single path component and thus doesn't need conversion.
|
||||
narrow_path
|
||||
} else {
|
||||
// Only call realpath() on the portion up to the last component.
|
||||
let narrow_res = if let Some(pathsep_idx) = pathsep_idx {
|
||||
// Only call realpath() on the portion up to the last component.
|
||||
canonicalize(OsStr::from_bytes(&narrow_path[0..pathsep_idx]))
|
||||
} else {
|
||||
// If there is no "/", this is a file in $PWD, so give the realpath to that.
|
||||
canonicalize(".")
|
||||
};
|
||||
|
||||
let Ok(narrow_result) = narrow_res else { return None; };
|
||||
|
||||
let pathsep_idx = pathsep_idx.map_or(0, |idx| idx + 1);
|
||||
|
||||
let mut real_path = narrow_result.into_os_string().into_vec();
|
||||
|
||||
// This test is to deal with cases such as /../../x => //x.
|
||||
if real_path.len() > 1 {
|
||||
real_path.push(b'/');
|
||||
}
|
||||
|
||||
real_path.extend_from_slice(&narrow_path[pathsep_idx..]);
|
||||
|
||||
real_path
|
||||
}
|
||||
};
|
||||
|
||||
let_cxx_string!(s = real_path);
|
||||
|
||||
Some(str2wcstring(&s).from_ffi())
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
||||
|
||||
[[package]]
|
||||
name = "widestring-suffix"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
[package]
|
||||
name = "widestring-suffix"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "1.0", features = ["full", "visit-mut"] }
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
extern crate proc_macro as pm;
|
||||
|
||||
use proc_macro2::{Group, Literal, TokenStream, TokenTree};
|
||||
use quote::quote_spanned;
|
||||
use syn::{Lit, LitStr};
|
||||
|
||||
/// A proc macro which allows easy creation of nul-terminated wide strings.
|
||||
/// It replaces strings with an L suffix like so:
|
||||
/// "foo"L
|
||||
/// with a call like so:
|
||||
/// crate::wchar::L!("foo")
|
||||
#[proc_macro_attribute]
|
||||
pub fn widestrs(_attr: pm::TokenStream, input: pm::TokenStream) -> pm::TokenStream {
|
||||
let s = widen_stream(input.into());
|
||||
s.into()
|
||||
}
|
||||
|
||||
fn widen_token_tree(tt: TokenTree) -> TokenStream {
|
||||
match tt {
|
||||
TokenTree::Group(group) => {
|
||||
let wide_stream = widen_stream(group.stream());
|
||||
TokenTree::Group(Group::new(group.delimiter(), wide_stream)).into()
|
||||
}
|
||||
TokenTree::Literal(lit) => widen_literal(lit),
|
||||
tt => tt.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn widen_stream(input: TokenStream) -> TokenStream {
|
||||
input.into_iter().map(widen_token_tree).collect()
|
||||
}
|
||||
|
||||
fn try_parse_literal(tt: TokenTree) -> Option<LitStr> {
|
||||
let ts: TokenStream = tt.into();
|
||||
match syn::parse2::<Lit>(ts) {
|
||||
Ok(Lit::Str(lit)) => Some(lit),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn widen_literal(lit: Literal) -> TokenStream {
|
||||
let tt = TokenTree::Literal(lit);
|
||||
match try_parse_literal(tt.clone()) {
|
||||
Some(lit) if lit.suffix() == "L" => {
|
||||
let value = lit.value();
|
||||
let span = lit.span();
|
||||
quote_spanned!(span=> crate::wchar::L!(#value)).into()
|
||||
}
|
||||
_ => tt.into(),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
use widestring_suffix::widestrs;
|
||||
|
||||
mod wchar {
|
||||
macro_rules! L {
|
||||
($string:expr) => {
|
||||
42
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use L;
|
||||
}
|
||||
|
||||
#[widestrs]
|
||||
mod stuff {
|
||||
pub fn test1() {
|
||||
let s = "abc"L;
|
||||
assert_eq!(s, 42);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_widestring() {
|
||||
stuff::test1();
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ URL: https://fishshell.com/
|
|||
|
||||
Source0: %{name}_@VERSION@.orig.tar.xz
|
||||
BuildRequires: ncurses-devel gettext gcc-c++ xz pcre2-devel
|
||||
BuildRequires: rust >= 1.67
|
||||
|
||||
%if 0%{?rhel} && 0%{?rhel} < 8
|
||||
BuildRequires: cmake3
|
||||
|
|
@ -29,6 +28,8 @@ BuildRequires: glibc-langpack-en
|
|||
%endif
|
||||
BuildRequires: python3 procps
|
||||
|
||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
|
||||
|
||||
%if 0%{?rhel} && 0%{?rhel} < 8
|
||||
Requires: python
|
||||
%else
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
function __fish_adb_no_subcommand -d 'Test if adb has yet to be given the subcommand'
|
||||
for i in (commandline -opc)
|
||||
if contains -- $i connect disconnect devices push pull sync shell emu logcat install uninstall jdwp forward bugreport backup restore version help start-server kill-server remount reboot get-state get-serialno get-devpath status-window root usb tcpip ppp sideload reconnect unroot
|
||||
if contains -- $i connect disconnect devices push pull sync shell emu logcat install uninstall jdwp forward bugreport backup restore version help wait-for-device start-server kill-server remount reboot get-state get-serialno get-devpath status-window root usb tcpip ppp sideload reconnect
|
||||
return 1
|
||||
end
|
||||
end
|
||||
|
|
@ -14,7 +14,7 @@ function __fish_adb_get_devices -d 'Run adb devices and parse output'
|
|||
set -l procs (ps -Ao comm= | string match 'adb')
|
||||
# Don't run adb devices unless the server is already started - it takes a while to init
|
||||
if set -q procs[1]
|
||||
adb devices -l | string replace -rf '(\S+).*product:(\S+).*model:(\S+).*' '$1'\t'$2 $3'
|
||||
adb devices -l | string replace -rf '(\S+).*model:(\S+).*' '$1'\t'$2'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -118,7 +118,6 @@ complete -f -n __fish_adb_no_subcommand -c adb -a get-serialno -d 'Prints serial
|
|||
complete -f -n __fish_adb_no_subcommand -c adb -a get-devpath -d 'Prints device path'
|
||||
complete -f -n __fish_adb_no_subcommand -c adb -a status-window -d 'Continuously print the device status'
|
||||
complete -f -n __fish_adb_no_subcommand -c adb -a root -d 'Restart the adbd daemon with root permissions'
|
||||
complete -f -n __fish_adb_no_subcommand -c adb -a unroot -d 'Restart the adbd daemon without root permissions'
|
||||
complete -f -n __fish_adb_no_subcommand -c adb -a usb -d 'Restart the adbd daemon listening on USB'
|
||||
complete -f -n __fish_adb_no_subcommand -c adb -a tcpip -d 'Restart the adbd daemon listening on TCP'
|
||||
complete -f -n __fish_adb_no_subcommand -c adb -a ppp -d 'Run PPP over USB'
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
set -l subcommands apk files manifest dex resources
|
||||
|
||||
set -l apk_subcommands summary file-size download-size features compare
|
||||
set -l files_subcommands list cat
|
||||
set -l manifest_subcommands print application-id version-name version-code min-sdk target-sdk permissions debuggable
|
||||
set -l dex_subcommands list references packages code
|
||||
set -l resources_subcommands package configs value name xml
|
||||
|
||||
|
||||
complete -f -n "not __fish_seen_subcommand_from $subcommands" -c apkanalyzer -a apk -d 'Analyze APK file attributes'
|
||||
complete -f -n "not __fish_seen_subcommand_from $subcommands" -c apkanalyzer -a files -d 'Analyze the files inside the APK file'
|
||||
complete -f -n "not __fish_seen_subcommand_from $subcommands" -c apkanalyzer -a manifest -d 'Analyze the contents of the manifest file'
|
||||
complete -f -n "not __fish_seen_subcommand_from $subcommands" -c apkanalyzer -a dex -d 'Analyze the DEX files inside the APK file'
|
||||
complete -f -n "not __fish_seen_subcommand_from $subcommands" -c apkanalyzer -a resources -d 'View text, image and string resources'
|
||||
|
||||
# global-option
|
||||
complete -n "not __fish_seen_subcommand_from $apk_subcommands $files_subcommands $manifest_subcommands $dex_subcommands $resources_subcommands" -c apkanalyzer -s h -l human-readable -d 'Human-readable output'
|
||||
|
||||
# apk
|
||||
complete -f -n "__fish_seen_subcommand_from apk; and not __fish_seen_subcommand_from $apk_subcommands" -c apkanalyzer -a summary -d 'Prints the application ID, version code, and version name'
|
||||
complete -f -n "__fish_seen_subcommand_from apk; and not __fish_seen_subcommand_from $apk_subcommands" -c apkanalyzer -a file-size -d 'Prints the total file size of the APK'
|
||||
complete -f -n "__fish_seen_subcommand_from apk; and not __fish_seen_subcommand_from $apk_subcommands" -c apkanalyzer -a download-size -d 'Prints an estimate of the download size of the APK'
|
||||
complete -f -n "__fish_seen_subcommand_from apk; and not __fish_seen_subcommand_from $apk_subcommands" -c apkanalyzer -a features -d 'Prints features used by the APK that trigger Play Store filtering'
|
||||
complete -f -n "__fish_seen_subcommand_from apk; and not __fish_seen_subcommand_from $apk_subcommands" -c apkanalyzer -a compare -d 'Compares the sizes of apk-file and apk-file'
|
||||
# apk options
|
||||
complete -n '__fish_seen_subcommand_from apk; and __fish_seen_subcommand_from features' -c apkanalyzer -l not-required -d 'Include features marked as not required in the output'
|
||||
complete -n '__fish_seen_subcommand_from apk; and __fish_seen_subcommand_from compare' -c apkanalyzer -l different-only -d 'Prints directories and files with differences'
|
||||
complete -n '__fish_seen_subcommand_from apk; and __fish_seen_subcommand_from compare' -c apkanalyzer -l files-only -d 'Does not print directory entries'
|
||||
complete -n '__fish_seen_subcommand_from apk; and __fish_seen_subcommand_from compare' -c apkanalyzer -l patch-size -d 'Shows an estimate of the file-by-file patch instead of a raw difference'
|
||||
|
||||
complete -n "__fish_seen_subcommand_from apk; and __fish_seen_subcommand_from $apk_subcommands" -c apkanalyzer -ka '(__fish_complete_suffix .apk)'
|
||||
|
||||
# files
|
||||
complete -f -n "__fish_seen_subcommand_from files; and not __fish_seen_subcommand_from $apk_subcommands" -c apkanalyzer -a list -d 'Lists all files in the APK'
|
||||
complete -f -n "__fish_seen_subcommand_from files; and not __fish_seen_subcommand_from $apk_subcommands" -c apkanalyzer -a cat -d 'Prints out the file contents'
|
||||
# files options
|
||||
complete -n '__fish_seen_subcommand_from files; and __fish_seen_subcommand_from list' -c apkanalyzer -l file -d 'Specify a path inside the APK' -r
|
||||
|
||||
complete -n "__fish_seen_subcommand_from files; and __fish_seen_subcommand_from $files_subcommands" -c apkanalyzer -ka '(__fish_complete_suffix .apk)'
|
||||
|
||||
# manifest
|
||||
complete -f -n "__fish_seen_subcommand_from manifest; and not __fish_seen_subcommand_from $manifest_subcommands" -c apkanalyzer -a print -d 'Prints the APK manifest in XML format'
|
||||
complete -f -n "__fish_seen_subcommand_from manifest; and not __fish_seen_subcommand_from $manifest_subcommands" -c apkanalyzer -a application-id -d 'Prints the application ID value'
|
||||
complete -f -n "__fish_seen_subcommand_from manifest; and not __fish_seen_subcommand_from $manifest_subcommands" -c apkanalyzer -a version-name -d 'Prints the version name value'
|
||||
complete -f -n "__fish_seen_subcommand_from manifest; and not __fish_seen_subcommand_from $manifest_subcommands" -c apkanalyzer -a version-code -d 'Prints the version code value'
|
||||
complete -f -n "__fish_seen_subcommand_from manifest; and not __fish_seen_subcommand_from $manifest_subcommands" -c apkanalyzer -a min-sdk -d 'Prints the minimum SDK version'
|
||||
complete -f -n "__fish_seen_subcommand_from manifest; and not __fish_seen_subcommand_from $manifest_subcommands" -c apkanalyzer -a target-sdk -d 'Prints the target SDK version'
|
||||
complete -f -n "__fish_seen_subcommand_from manifest; and not __fish_seen_subcommand_from $manifest_subcommands" -c apkanalyzer -a permissions -d 'Prints the list of permissions'
|
||||
complete -f -n "__fish_seen_subcommand_from manifest; and not __fish_seen_subcommand_from $manifest_subcommands" -c apkanalyzer -a debuggable -d 'Prints whether the APK is debuggable'
|
||||
|
||||
complete -n "__fish_seen_subcommand_from manifest; and __fish_seen_subcommand_from $manifest_subcommands" -c apkanalyzer -ka '(__fish_complete_suffix .apk)'
|
||||
|
||||
# dex
|
||||
complete -f -n "__fish_seen_subcommand_from dex; and not __fish_seen_subcommand_from $dex_subcommands" -c apkanalyzer -a list -d 'Prints a list of the DEX files in the APK'
|
||||
complete -f -n "__fish_seen_subcommand_from dex; and not __fish_seen_subcommand_from $dex_subcommands" -c apkanalyzer -a references -d 'Prints the number of method references in the specified DEX files'
|
||||
complete -f -n "__fish_seen_subcommand_from dex; and not __fish_seen_subcommand_from $dex_subcommands" -c apkanalyzer -a packages -d 'Prints the class tree from DEX'
|
||||
complete -f -n "__fish_seen_subcommand_from dex; and not __fish_seen_subcommand_from $dex_subcommands" -c apkanalyzer -a code -d 'Prints the bytecode of a class or method in smali format'
|
||||
# dex options
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from references' -c apkanalyzer -l files -d 'Indicate specific files that you want to include' -r
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from packages' -c apkanalyzer -l defined-only -d 'Includes only classes defined in the APK in the output'
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from packages' -c apkanalyzer -l files -d 'Specifies the DEX file names to include'
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from packages' -c apkanalyzer -l proguard-folder -d 'Specifies the Proguard output folder to search for mappings'
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from packages' -c apkanalyzer -l proguard-mapping -d 'Specifies the Proguard mapping file'
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from packages' -c apkanalyzer -l proguard-seeds -d 'Specifies the Proguard seeds file'
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from packages' -c apkanalyzer -l proguard-usage -d 'Specifies the Proguard usage file'
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from packages' -c apkanalyzer -l show-removed -d 'Shows classes and members that were removed by Proguard'
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from code' -c apkanalyzer -l class -d 'Specifies the class name to print'
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from code' -c apkanalyzer -l method -d 'Specifies the method name to print'
|
||||
|
||||
complete -n "__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from $dex_subcommands" -c apkanalyzer -ka '(__fish_complete_suffix .apk)'
|
||||
complete -n '__fish_seen_subcommand_from dex; and __fish_seen_subcommand_from code' -c apkanalyzer -ka '(__fish_complete_suffix .class)'
|
||||
|
||||
# resources
|
||||
complete -f -n "__fish_seen_subcommand_from resources; and not __fish_seen_subcommand_from $resources_subcommands" -c apkanalyzer -a packages -d 'Prints a list of the packages that are defined in the resources table'
|
||||
complete -f -n "__fish_seen_subcommand_from resources; and not __fish_seen_subcommand_from $resources_subcommands" -c apkanalyzer -a configs -d 'Prints a list of configurations for the specified type'
|
||||
complete -f -n "__fish_seen_subcommand_from resources; and not __fish_seen_subcommand_from $resources_subcommands" -c apkanalyzer -a value -d 'Prints the value of the resource specified by config, name, and type'
|
||||
complete -f -n "__fish_seen_subcommand_from resources; and not __fish_seen_subcommand_from $resources_subcommands" -c apkanalyzer -a names -d 'Prints a list of resource names for a configuration and type'
|
||||
complete -f -n "__fish_seen_subcommand_from resources; and not __fish_seen_subcommand_from $resources_subcommands" -c apkanalyzer -a xml -d 'Prints the human-readable form of a binary XML file'
|
||||
# resources options
|
||||
complete -n '__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from configs' -c apkanalyzer -l type -d 'Specifies the resource type to print' -r
|
||||
complete -n '__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from configs' -c apkanalyzer -l packages -d 'Specifies the packages to print' -r
|
||||
complete -n '__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from value' -c apkanalyzer -l config -d 'Specifies the configuration to print' -r
|
||||
complete -n '__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from value' -c apkanalyzer -l name -d 'Specifies the resource name to print' -r
|
||||
complete -n '__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from value' -c apkanalyzer -l type -d 'Specifies the resource type to print' -r
|
||||
complete -n '__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from value' -c apkanalyzer -l packages -d 'Specifies the packages to print' -r
|
||||
complete -n '__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from names' -c apkanalyzer -l config -d 'Specifies the configuration to print' -r
|
||||
complete -n '__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from names' -c apkanalyzer -l type -d 'Specifies the resource type to print' -r
|
||||
complete -n '__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from names' -c apkanalyzer -l packages -d 'Specifies the packages to print' -r
|
||||
complete -n '__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from xml' -c apkanalyzer -l file -d 'Specifies the file to print' -r
|
||||
|
||||
complete -n "__fish_seen_subcommand_from resources; and __fish_seen_subcommand_from $resources_subcommands" -c apkanalyzer -ka '(__fish_complete_suffix .apk)'
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Completions for the `apt` command
|
||||
|
||||
# macOS has a /usr/bin/apt that is something else entirely:
|
||||
# macOS has a /usr/bin/apt that is something else entirely:
|
||||
# (apt - Returns the path to a Java home directory from the current user's settings)
|
||||
if [ "$(uname -s)" = Darwin -a "$(command -s apt)" = /usr/bin/apt ]
|
||||
exit 1
|
||||
|
|
@ -14,7 +14,7 @@ set -l handle_file_pkg_subcmds install
|
|||
function __fish_apt_subcommand -V all_subcmds
|
||||
set -l subcommand $argv[1]
|
||||
set -e argv[1]
|
||||
complete -f -c apt -n "__fish_is_first_token" -a $subcommand $argv
|
||||
complete -f -c apt -n "not __fish_seen_subcommand_from $all_subcmds" -a $subcommand $argv
|
||||
end
|
||||
|
||||
function __fish_apt_option
|
||||
|
|
@ -33,10 +33,9 @@ end
|
|||
|
||||
complete -c apt -f
|
||||
|
||||
# We use -k to keep PWD directories (from the .deb completion) after packages, so we need to sort the packages
|
||||
complete -k -c apt -n "__fish_seen_subcommand_from $handle_file_pkg_subcmds" -kxa '(__fish_complete_suffix .deb)'
|
||||
complete -k -c apt -n "__fish_seen_subcommand_from $pkg_subcmds" -kxa '(__fish_print_apt_packages | sort)'
|
||||
complete -c apt -n "__fish_seen_subcommand_from $installed_pkg_subcmds" -kxa '(__fish_print_apt_packages --installed | sort)'
|
||||
complete -k -c apt -n "__fish_seen_subcommand_from $pkg_subcmds" -a '(__fish_print_apt_packages | string match -re -- "(?:\\b|_)"(commandline -ct | string escape --style=regex) | head -n 250 | sort)'
|
||||
complete -c apt -n "__fish_seen_subcommand_from $installed_pkg_subcmds" -a '(__fish_print_apt_packages --installed | string match -re -- "(?:\\b|_)"(commandline -ct | string escape --style=regex) | head -n 250)'
|
||||
complete -k -c apt -n "__fish_seen_subcommand_from $handle_file_pkg_subcmds" -a '(__fish_complete_suffix .deb)'
|
||||
|
||||
complete -c apt -n "__fish_seen_subcommand_from install" -l no-install-recommends
|
||||
# This advanced flag is the safest way to upgrade packages that otherwise would have been kept back
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
complete -x -c asciidoctor -k -a "(__fish_complete_suffix .asciidoc .adoc .ad .asc .txt)"
|
||||
complete -x -c asciidoctor -k -a "
|
||||
(
|
||||
__fish_complete_suffix .asciidoc
|
||||
__fish_complete_suffix .adoc
|
||||
__fish_complete_suffix .ad
|
||||
__fish_complete_suffix .asc
|
||||
__fish_complete_suffix .txt
|
||||
)
|
||||
"
|
||||
|
||||
# Security Settings
|
||||
complete -c asciidoctor -s B -l base-dir -d "Base directory containing the document"
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue