Merge branch 'master' into rewrite/webconfig-to-alpinejs

This commit is contained in:
Fabian Boehm
2023-08-29 14:25:02 +02:00
committed by GitHub
597 changed files with 60765 additions and 30049 deletions

View File

@@ -51,7 +51,8 @@ linux_task:
- ninja -j 6 fish fish_tests
- ninja fish_run_tests
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'
# CI task disabled during RIIR transition
only_if: false && $CIRRUS_REPO_OWNER == 'fish-shell'
linux_arm_task:
matrix:
@@ -74,21 +75,24 @@ linux_arm_task:
- file ./fish
- ninja fish_run_tests
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'
# CI task disabled during RIIR transition
only_if: false && $CIRRUS_REPO_OWNER == 'fish-shell'
freebsd_task:
matrix:
- name: FreeBSD 14
freebsd_instance:
image_family: freebsd-14-0-snap
# - name: FreeBSD 14
# freebsd_instance:
# image_family: freebsd-14-0-snap
- name: FreeBSD 13
freebsd_instance:
image: freebsd-13-1-release-amd64
- name: FreeBSD 12.3
freebsd_instance:
image: freebsd-12-3-release-amd64
image: freebsd-13-2-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
# 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
@@ -99,8 +103,16 @@ 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 -s ninja -j 6 fish fish_tests
- sudo -u fish-user -s ninja fish_run_tests
- sudo -u fish-user sh -c '. $HOME/.cargo/env; ninja -j 6 fish fish_tests'
- sudo -u fish-user sh -c '. $HOME/.cargo/env; ninja fish_run_tests'
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'

View File

@@ -22,7 +22,7 @@ indent_size = 2
indent_size = 2
[share/{completions,functions}/**.fish]
max_line_length = none
max_line_length = off
[{COMMIT_EDITMSG,git-revise-todo}]
max_line_length = 80

View File

@@ -8,7 +8,7 @@ Please tell us which operating system and terminal you are using. The output of
Please tell us if you tried fish without third-party customizations by executing this command and whether it affected the behavior you are reporting:
sh -c 'env HOME=$(mktemp -d) fish'
sh -c 'env HOME=$(mktemp -d) XDG_CONFIG_HOME= fish'
Tell us how to reproduce the problem. Including an asciinema.org recording is useful for problems that involve the visual display of fish output such as its prompt.
-->

View File

@@ -16,6 +16,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@1.67
- name: Install deps
run: |
sudo apt install gettext libncurses5-dev libpcre2-dev python3-pip tmux
@@ -42,6 +43,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@1.67
with:
targets: "i686-unknown-linux-gnu" # rust-toolchain wants this comma-separated
- name: Install deps
run: |
sudo apt update
@@ -53,10 +57,10 @@ jobs:
CFLAGS: "-m32"
run: |
mkdir build && cd build
cmake -DFISH_USE_SYSTEM_PCRE2=OFF ..
cmake -DFISH_USE_SYSTEM_PCRE2=OFF -DRust_CARGO_TARGET=i686-unknown-linux-gnu ..
- name: make
run: |
make
make VERBOSE=1
- name: make test
run: |
make test
@@ -64,13 +68,29 @@ 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
# All -Z options require running nightly
- uses: dtolnay/rust-toolchain@nightly
with:
# ASAN uses `cargo build -Zbuild-std` which requires the rust-src component
# this is comma-separated
components: rust-src
- name: Install deps
run: |
sudo apt install gettext libncurses5-dev libpcre2-dev python3-pip tmux
sudo pip3 install pexpect
# Don't install pexpect here because this constantly blows the time budget.
# Try again once the rust port is done and we're hopefully not as slow anymore.
# sudo pip3 install pexpect
- name: cmake
env:
CC: clang
@@ -78,48 +98,57 @@ jobs:
CXXFLAGS: "-fno-omit-frame-pointer -fsanitize=undefined -fsanitize=address -DFISH_CI_SAN"
run: |
mkdir build && cd build
cmake ..
# 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).
env CXXFLAGS="$CXXFLAGS -fsanitize-blacklist=$PWD/../build_tools/asan_blacklist.txt" \
cmake .. -DASAN=1 -DRust_CARGO_TARGET=x86_64-unknown-linux-gnu -DCMAKE_BUILD_TYPE=Debug
- name: make
run: |
make
- name: make test
env:
FISH_CI_SAN: 1
ASAN_OPTIONS: check_initialization_order=1:detect_stack_use_after_return=1:detect_leaks=1
ASAN_OPTIONS: check_initialization_order=1:detect_stack_use_after_return=1:detect_leaks=1:fast_unwind_on_malloc=0
UBSAN_OPTIONS: print_stacktrace=1:report_error_type=1
# use_tls=0 is a workaround for LSAN crashing with "Tracer caught signal 11" (SIGSEGV),
# which seems to be an issue with TLS support in newer glibc versions under virtualized
# environments. Follow https://github.com/google/sanitizers/issues/1342 and
# https://github.com/google/sanitizers/issues/1409 to track this issue.
LSAN_OPTIONS: verbosity=0:log_threads=0:use_tls=0
# UPDATE: this can cause spurious leak reports for __cxa_thread_atexit_impl() under glibc.
LSAN_OPTIONS: verbosity=0:log_threads=0:use_tls=1:print_suppressions=0
run: |
make test
env LSAN_OPTIONS="$LSAN_OPTIONS:suppressions=$PWD/build_tools/lsan_suppressions.txt" make test
ubuntu-threadsan:
# 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.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- 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
# ubuntu-threadsan:
#
# runs-on: ubuntu-latest
#
# steps:
# - uses: actions/checkout@v3
# - uses: dtolnay/rust-toolchain@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
macos:
@@ -127,6 +156,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@1.67
- name: Install deps
run: |
sudo pip3 install pexpect

32
.github/workflows/rust_checks.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Rust checks
on: [push, pull_request]
permissions:
contents: read
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- name: cargo fmt
run: cargo fmt --check --all
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@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: cargo clippy --workspace --all-targets -- --deny=warnings

13
.gitignore vendored
View File

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

View File

@@ -3,53 +3,71 @@ fish 3.7.0 (released ???)
.. ignore: 9439 9440 9442 9452 9469 9480 9482
Notable backwards-incompatible changes
--------------------------------------
fish is being (once you are reading this hopefully "has been") ported to rust, which unfortunately involves a few backwards-incompatible changes.
We have tried to keep these to a minimum, but in some cases it is unavoidable.
- ``random`` now uses a different random number generator and so the values you get even with the same seed have changed.
Notably, it will now work much more sensibly with very small seeds.
The seed was never guaranteed to give the same result across systems,
so we do not expect this to have a large impact (:issue:`9593`).
- ``functions --handlers`` will now list handlers in a different order.
Now it is definition order, first to last, where before it was last to first.
This was never specifically defined, and we recommend not relying on a specific order (:issue:`9944`).
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`).
- Selected text (for example, in vi visual mode) now respects the foreground color and other options such as bold (:issue:`9717`).
- An issue where the pager would not show the last item after pressing the up arrow key has been fixed (:issue:`9833`).
New or improved bindings
^^^^^^^^^^^^^^^^^^^^^^^^
- The ``E`` binding in vi mode now correctly handles the last character of the word, by jumping to the next word (:issue:`9700`).
Improved prompts
^^^^^^^^^^^^^^^^
Completions
^^^^^^^^^^^
- Added completions for:
- ``otool``
- ``mix phx``
- git's completion for ``git-foo``-style commands was fixed (:issue:`9457`)
- File completion now offers ``../`` and ``./`` again (:issue:`9477`)
- Added or improved completions for:
- ``ar`` (:issue:`9719`)
- ``gcc`` completion descriptions have been clarified and shortened (:issue:`9722`).
- ``qdbus`` completions now properly handle tags (:issue:`9776`).
- ``age`` (:issue:`9813`).
- ``age-keygen`` (:issue:`9813`).
- ``curl`` (:issue:`9863`).
- ``krita`` (:issue:`9903`).
- ``blender`` (:issue:`9905`).
- ``gimp`` (:issue:`9904`).
- ``horcrux`` (:issue:`9922`).
Improved terminal support
^^^^^^^^^^^^^^^^^^^^^^^^^
Other improvements
------------------
- A bug that prevented certain executables from being offered in tab-completions when root has been fixed (:issue:`9639`).
- Builin `jobs` will print commands with non-printable chars escaped (:issue:`9808`)
- An integer overflow in `string repeat` leading to a near-infinite loop has been fixed (:issue:`9899`).
- `string shorten` behaves better in the presence of non-printable characters, including fixing an integer overflow that shortened strings more than intended. (:issue:`9854`)
- `string pad` no longer allows non-printable characters as padding. (:issue:`9854`)
- PWD reporting via OSC 7 is now enabled by default for iTerm2.
For distributors
----------------
@@ -57,6 +75,77 @@ For distributors
--------------
fish 3.6.1 (released March 25, 2023)
====================================
This release of fish contains a number of fixes for problems identified in fish 3.6.0, as well as some enhancements.
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`).
Deprecations and removed features
---------------------------------
- The Web-based configuration for abbreviations has been removed, as it was not functional with the changes abbreviations introduced in 3.6.0 (:issue:`9460`).
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`).
- ``set_color -v`` no longer crashes fish (:issue:`9640`).
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`)
- Variables that were set while the locale was C (the default ASCII-only locale) will now properly be encoded if the locale is switched (:issue:`2613`, :issue:`9473`).
- Escape during history search restores the original command line again (fixing a regression in 3.6.0).
- Using ``--help`` on builtins now respects the ``$MANPAGER`` variable, in preference to ``$PAGER`` (:issue:`9488`).
- :kbd:`Control-G` closes the history pager, like other shells (:issue:`9484`).
- The documentation for the ``:``, ``[`` and ``.`` builtin commands can now be looked up with ``man`` (:issue:`9552`).
- fish no longer crashes when searching history for non-ASCII codepoints case-insensitively (:issue:`9628`).
- The :kbd:`Alt-S` binding will now also use ``please`` if available (:issue:`9635`).
- Themes that don't specify every color option can be installed correctly in the Web-based configuration (:issue:`9590`).
- Compatibility with Midnight Commander's prompt integration has been improved (:issue:`9540`).
- A spurious error, noted when using fish in Google Drive directories under WSL 2, has been silenced (:issue:`9550`).
- Using ``read`` in ``fish_greeting`` or similar functions will not trigger an infinite loop (:issue:`9564`).
- Compatibility when upgrading from old versions of fish (before 3.4.0) has been improved (:issue:`9569`).
Improved prompts
^^^^^^^^^^^^^^^^
- The git prompt will compute the stash count to be used independently of the informative status (:issue:`9572`).
Completions
^^^^^^^^^^^
- Added completions for:
- ``apkanalyzer`` (:issue:`9558`)
- ``neovim`` (:issue:`9543`)
- ``otool``
- ``pre-commit`` (:issue:`9521`)
- ``proxychains`` (:issue:`9486`)
- ``scrypt`` (:issue:`9583`)
- ``stow`` (:issue:`9571`)
- ``trash`` and helper utilities ``trash-empty``, ``trash-list``, ``trash-put``, ``trash-restore`` (:issue:`9560`)
- ``ssh-copy-id`` (:issue:`9675`)
- Improvements to many completions, including the speed of completing directories in WSL 2 (:issue:`9574`).
- Completions using ``__fish_complete_suffix`` are now offered in the correct order, fixing a regression in 3.6.0 (:issue:`8924`).
- ``git`` completions for ``git-foo``-style commands was restored, fixing a regression in 3.6.0 (:issue:`9457`).
- File completion now offers ``../`` and ``./`` again, fixing a regression in 3.6.0 (:issue:`9477`).
- The behaviour of completions using ``__fish_complete_path`` matches standard path completions (:issue:`9285`).
Other improvements
------------------
- Improvements and corrections to the documentation.
For distributors
----------------
- fish 3.6.1 builds correctly on Cygwin (:issue:`9502`).
--------------
fish 3.6.0 (released January 7, 2023)
=====================================

View File

@@ -24,6 +24,8 @@ 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")
@@ -43,6 +45,9 @@ 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()
@@ -53,6 +58,9 @@ 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
@@ -91,36 +99,32 @@ endif()
# List of sources for builtin functions.
set(FISH_BUILTIN_SRCS
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/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/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)
src/builtin.cpp src/builtins/bind.cpp
src/builtins/commandline.cpp src/builtins/complete.cpp
src/builtins/disown.cpp
src/builtins/eval.cpp src/builtins/fg.cpp
src/builtins/history.cpp
src/builtins/jobs.cpp
src/builtins/read.cpp src/builtins/set.cpp
src/builtins/source.cpp
src/builtins/ulimit.cpp
)
# List of other sources.
set(FISH_SRCS
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/fd_monitor.cpp src/fish_version.cpp
src/flog.cpp src/function.cpp src/future_feature_flags.cpp src/highlight.cpp
src/ast.cpp src/autoload.cpp src/color.cpp src/common.cpp src/complete.cpp
src/env.cpp src/env_universal_common.cpp src/event.cpp
src/exec.cpp src/expand.cpp src/fallback.cpp src/fish_indent_common.cpp src/fish_version.cpp
src/flog.cpp src/function.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/job_group.cpp src/kill.cpp
src/io.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/pager.cpp src/parse_execution.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/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
src/proc.cpp src/reader.cpp src/screen.cpp
src/signals.cpp src/utf8.cpp
src/wcstringutil.cpp src/wgetopt.cpp src/wildcard.cpp
src/wutil.cpp src/fds.cpp src/rustffi.cpp
)
# Header files are just globbed.
@@ -133,6 +137,11 @@ 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
@@ -175,8 +184,10 @@ 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})
${PCRE2_LIB} ${Intl_LIBRARIES} ${ATOMIC_LIBRARY}
"fish-rust")
target_include_directories(fishlib PRIVATE
${CURSES_INCLUDE_DIRS})
@@ -186,12 +197,12 @@ fish_link_deps_and_sign(fish)
# Define fish_indent.
add_executable(fish_indent
src/fish_indent.cpp src/print_help.cpp)
src/fish_indent.cpp)
fish_link_deps_and_sign(fish_indent)
# Define fish_key_reader.
add_executable(fish_key_reader
src/fish_key_reader.cpp src/print_help.cpp)
src/fish_key_reader.cpp)
fish_link_deps_and_sign(fish_key_reader)
# Set up the docs.

View File

@@ -1,9 +1,46 @@
Guidelines For Developers
=========================
####################
Contributing To Fish
####################
This document provides guidelines for making changes to the fish-shell
project. This includes rules for how to format the code, naming
conventions, et cetera.
This document tells you how you can contribute to fish.
Fish is free and open source software, distributed under the terms of the GPLv2.
Contributions are welcome, and there are many ways to contribute!
Whether you want to change some of the core rust/C++ source, enhance or add a completion script or function,
improve the documentation or translate something, this document will tell you how.
Getting Set Up
==============
Fish is developed on Github, at https://github.com/fish-shell/fish-shell.
First, you'll need an account there, and you'll need a git clone of fish.
Fork it on Github and then run::
git clone https://github.com/<USERNAME>/fish-shell.git
This will create a copy of the fish repository in the directory fish-shell in your current working directory.
Also, for most changes you want to run the tests and so you'd get a setup to compile fish.
For that, you'll require:
- Rust (version 1.67 or later) - when in doubt, try rustup
- 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)
- PCRE2 (headers and libraries) - optional, this will be downloaded if missing
- gettext (headers and libraries) - optional, for translation support
- Sphinx - optional, to build the documentation
Of course not everything is required always - if you just want to contribute something to the documentation you'll just need Sphinx,
and if the change is very simple and obvious you can just send it in. Use your judgement!
Once you have your changes, open a pull request on https://github.com/fish-shell/fish-shell/pulls.
Guidelines
==========
In short:
@@ -11,7 +48,7 @@ In short:
- Use automated tools to help you (including ``make test``, ``build_tools/style.fish`` and ``make lint``)
Contributing completions
------------------------
========================
Completion scripts are the most common contribution to fish, and they are very welcome.
@@ -42,16 +79,29 @@ Put your completion script into share/completions/name-of-command.fish. If you h
If you want to add tests, you probably want to add a littlecheck test. See below for details.
Contributing to fish's C++ core
-------------------------------
Contributing documentation
==========================
Fish uses C++11. Newer C++ features should not be used to make it possible to use on older systems.
The documentation is stored in ``doc_src/``, and written in ReStructured Text and built with Sphinx.
It does not use exceptions, they are disabled at build time with ``-fno-exceptions``.
To build it locally, run from the main fish-shell directory::
Don't introduce new dependencies unless absolutely necessary, and if you do,
please make it optional with graceful failure if possible.
Add any new dependencies to the README.rst under the *Running* and/or *Building* sections.
sphinx-build -j 8 -b html -n doc_src/ /tmp/fish-doc/
which will build the docs as html in /tmp/fish-doc. You can open it in a browser and see that it looks okay.
The builtins and various functions shipped with fish are documented in doc_src/cmds/.
Contributing to fish's Rust/C++ core
====================================
As of now, fish is in the process of switching from C++11 to Rust, so this is in flux.
See doc_internal/rust-devel.md for some information on the port.
Importantly, the initial port strives for fidelity with the existing C++ codebase,
so it won't be 100% idiomatic rust - in some cases it'll have some awkward interface code
in order to interact with the C++.
Linters
-------
@@ -70,29 +120,10 @@ help catch mistakes such as using ``wcwidth()`` rather than
``fish_wcwidth()``. Please add a new rule if you find similar mistakes
being made.
Suppressing Lint Warnings
~~~~~~~~~~~~~~~~~~~~~~~~~
Once in a while the lint tools emit a false positive warning. For
example, cppcheck might suggest a memory leak is present when that is
not the case. To suppress that cppcheck warning you should insert a line
like the following immediately prior to the line cppcheck warned about:
::
// cppcheck-suppress memleak // addr not really leaked
The explanatory portion of the suppression comment is optional. For
other types of warnings replace “memleak” with the value inside the
parenthesis (e.g., “nullPointerRedundantCheck”) from a warning like the
following:
::
[src/complete.cpp:1727]: warning (nullPointerRedundantCheck): Either the condition 'cmd_node' is redundant or there is possible null pointer dereference: cmd_node.
We use ``clippy`` for Rust.
Code Style
----------
==========
To ensure your changes conform to the style rules run
@@ -122,6 +153,20 @@ If you want to check the style of the entire code base run
That command will refuse to restyle any files if you have uncommitted
changes.
Fish Script Style Guide
-----------------------
1. All fish scripts, such as those in the *share/functions* and *tests*
directories, should be formatted using the ``fish_indent`` command.
2. Function names should be in all lowercase with words separated by
underscores. Private functions should begin with an underscore. The
first word should be ``fish`` if the function is unique to fish.
3. The first word of global variable names should generally be ``fish``
for public vars or ``_fish`` for private vars to minimize the
possibility of name clashes with user defined vars.
Configuring Your Editor for Fish Scripts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -158,20 +203,6 @@ made to run fish_indent via e.g.
(add-hook 'fish-mode-hook (lambda ()
(add-hook 'before-save-hook 'fish_indent-before-save)))
Fish Script Style Guide
-----------------------
1. All fish scripts, such as those in the *share/functions* and *tests*
directories, should be formatted using the ``fish_indent`` command.
2. Function names should be in all lowercase with words separated by
underscores. Private functions should begin with an underscore. The
first word should be ``fish`` if the function is unique to fish.
3. The first word of global variable names should generally be ``fish``
for public vars or ``_fish`` for private vars to minimize the
possibility of name clashes with user defined vars.
C++ Style Guide
---------------
@@ -215,8 +246,13 @@ or
/// brief description of somefunction()
void somefunction() {
Rust Style Guide
----------------
Use ``cargo fmt`` and ``cargo clippy``. Clippy warnings can be turned off if there's a good reason to.
Testing
-------
=======
The source code for fish includes a large collection of tests. If you
are making any changes to fish, running these tests is a good way to make
@@ -241,7 +277,7 @@ fish_tests.cpp is mostly useful for unit tests - if you wish to test that a func
The pexpects are written in python and can simulate input and output to/from a terminal, so they are needed for anything that needs actual interactivity. The runner is in build_tools/pexpect_helper.py, in case you need to modify something there.
Local testing
~~~~~~~~~~~~~
-------------
The tests can be run on your local computer on all operating systems.
@@ -251,7 +287,7 @@ The tests can be run on your local computer on all operating systems.
make test
Git hooks
~~~~~~~~~
---------
Since developers sometimes forget to run the tests, it can be helpful to
use git hooks (see githooks(5)) to automate it.
@@ -294,7 +330,7 @@ To install the hook, place the code in a new file
``.git/hooks/pre-push`` and make it executable.
Coverity Scan
~~~~~~~~~~~~~
-------------
We use Coveritys static analysis tool which offers free access to open
source projects. While access to the tool itself is restricted,
@@ -304,46 +340,64 @@ with their GitHub account. Currently, tests are triggered upon merging
the ``master`` branch into ``coverity_scan_master``. Even if you are not
a fish developer, you can keep an eye on our statistics there.
Installing the Required Tools
-----------------------------
Installing the Linting Tools
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To install the lint checkers on Mac OS X using Homebrew:
::
brew install cppcheck
To install the lint checkers on Debian-based Linux distributions:
::
sudo apt-get install clang
sudo apt-get install cppcheck
Installing the Formatting Tools
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Mac OS X:
::
brew install clang-format
Debian-based:
::
sudo apt-get install clang-format
Message Translations
--------------------
Contributing Translations
=========================
Fish uses the GNU gettext library to translate messages from English to
other languages.
Creating and updating translations requires the Gettext tools, including
``xgettext``, ``msgfmt`` and ``msgmerge``. Translation sources are
stored in the ``po`` directory, named ``LANG.po``, where ``LANG`` is the
two letter ISO 639-1 language code of the target language (eg ``de`` for
German).
To create a new translation:
* generate a ``messages.pot`` file by running ``build_tools/fish_xgettext.fish`` from
the source tree
* copy ``messages.pot`` to ``po/LANG.po``
To update a translation:
* generate a ``messages.pot`` file by running
``build_tools/fish_xgettext.fish`` from the source tree
* update the existing translation by running
``msgmerge --update --no-fuzzy-matching po/LANG.po messages.pot``
The ``--no-fuzzy-matching`` is important as we have had terrible experiences with gettext's "fuzzy" translations in the past.
Many tools are available for editing translation files, including
command-line and graphical user interface programs. For simple use, you can just use your text editor.
Open up the po file, for example ``po/sv.po``, and you'll see something like::
msgid "%ls: No suitable job\n"
msgstr ""
The ``msgid`` here is the "name" of the string to translate, typically the english string to translate. The second line (``msgstr``) is where your translation goes.
For example::
msgid "%ls: No suitable job\n"
msgstr "%ls: Inget passande jobb\n"
Any ``%s`` / ``%ls`` or ``%d`` are placeholders that fish will use for formatting at runtime. It is important that they match - the translated string should have the same placeholders in the same order.
Also any escaped characters, like that ``\n`` newline at the end, should be kept so the translation has the same behavior.
Our tests run ``msgfmt --check-format /path/to/file``, so they would catch mismatched placeholders - otherwise fish would crash at runtime when the string is about to be used.
Be cautious about blindly updating an existing translation file. Trivial
changes to an existing message (eg changing the punctuation) will cause
existing translations to be removed, since the tools do literal string
matching. Therefore, in general, you need to carefully review any
recommended deletions.
Setting Code Up For Translations
--------------------------------
All non-debug messages output for user consumption should be marked for
translation. In C++, this requires the use of the ``_`` (underscore)
macro:
@@ -353,7 +407,8 @@ macro:
streams.out.append_format(_(L"%ls: There are no jobs\n"), argv[0]);
All messages in fish script must be enclosed in single or double quote
characters. They must also be translated via a subcommand. This means
characters for our message extraction script to find them.
They must also be translated via a command substitution. This means
that the following are **not** valid:
::
@@ -368,82 +423,15 @@ Above should be written like this instead:
echo (_ "hello")
echo (_ "goodbye")
Note that you can use either single or double quotes to enclose the
You can use either single or double quotes to enclose the
message to be translated. You can also optionally include spaces after
the opening parentheses and once again before the closing parentheses.
Creating and updating translations requires the Gettext tools, including
``xgettext``, ``msgfmt`` and ``msgmerge``. Translation sources are
stored in the ``po`` directory, named ``LANG.po``, where ``LANG`` is the
two letter ISO 639-1 language code of the target language (eg ``de`` for
German).
To create a new translation, for example for German:
* generate a ``messages.pot`` file by running ``build_tools/fish_xgettext.fish`` from
the source tree
* copy ``messages.pot`` to ``po/LANG.po``
To update a translation:
* generate a ``messages.pot`` file by running
``build_tools/fish_xgettext.fish`` from the source tree
* update the existing translation by running
``msgmerge --update --no-fuzzy-matching po/LANG.po messages.pot``
Many tools are available for editing translation files, including
command-line and graphical user interface programs.
Be cautious about blindly updating an existing translation file. Trivial
changes to an existing message (eg changing the punctuation) will cause
existing translations to be removed, since the tools do literal string
matching. Therefore, in general, you need to carefully review any
recommended deletions.
Read the `translations
wiki <https://github.com/fish-shell/fish-shell/wiki/Translations>`__ for
more information.
the opening parentheses or before the closing parentheses.
Versioning
----------
==========
The fish version is constructed by the *build_tools/git_version_gen.sh*
script. For developers the version is the branch name plus the output of
``git describe --always --dirty``. Normally the main part of the version
will be the closest annotated tag. Which itself is usually the most
recent release number (e.g., ``2.6.0``).
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 "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
(or header) that includes a particular header.
To help enforce this rule the ``make lint`` (and ``make lint-all``)
command will run the
`include-what-you-use <https://include-what-you-use.org/>`__ tool. You
can find the IWYU project on
`github <https://github.com/include-what-you-use/include-what-you-use>`__.
To install the tool on OS X youll need to add a
`formula <https://github.com/jasonmp85/homebrew-iwyu>`__ then install
it:
::
brew tap jasonmp85/iwyu
brew install iwyu
On Ubuntu you can install it via ``apt-get``:
::
sudo apt-get install iwyu

View File

@@ -9,8 +9,9 @@ Most of fish is licensed under the GNU General Public License version 2, and
you can redistribute it and/or modify it under the terms of the GNU GPL as
published by the Free Software Foundation.
fish also includes software licensed under the GNU Lesser General Public
License version 2, the OpenBSD license, the ISC license, and the NetBSD license.
fish also includes software licensed under the CMake license, the Python
Software Foundation License version 2, the OpenBSD license, the ISC license,
the NetBSD license, and the MIT license.
Full licensing information is contained in doc_src/license.rst.

1126
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

31
Cargo.toml Normal file
View File

@@ -0,0 +1,31 @@
[workspace]
resolver = "2"
members = [
"fish-rust",
"fish-rust/widestring-suffix",
]
default-members = ["fish-rust"]
[workspace.package]
rust-version = "1.67"
edition = "2021"
# TODO: Move fish-rust to src, make it the root package of this workspace
[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" }
[profile.release]
overflow-checks = true

View File

@@ -146,7 +146,7 @@ Building
Dependencies
~~~~~~~~~~~~
Compiling fish requires:
Compiling fish from a tarball requires:
- a C++11 compiler (g++ 4.8 or later, or clang 3.3 or later)
- CMake (version 3.5 or later)
@@ -159,6 +159,20 @@ cloned git repository.
Additionally, running the test suite requires Python 3.5+ and the pexpect package.
Dependencies, git master
~~~~~~~~~~~~~~~~~~~~~~~~
Building from git master currently requires, in addition to the dependencies for a tarball:
- Rust (version 1.67 or later)
- libclang, even if you are compiling with GCC
- an Internet connection
fish is in the process of being ported to Rust, replacing all C++ code, and as such these dependencies are a bit awkward and in flux.
In general, we would currently not recommend running from git master if you just want to *use* fish.
Given the nature of the port, what is currently there is mostly a slower and buggier version of the last C++-based release.
Building from source (all platforms) - Makefile generator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -174,34 +188,12 @@ To install into ``/usr/local``, run:
The install directory can be changed using the
``-DCMAKE_INSTALL_PREFIX`` parameter for ``cmake``.
Building from source (macOS) - Xcode
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Note: The minimum supported macOS version is 10.10 "Yosemite".
.. code:: bash
mkdir build; cd build
cmake .. -G Xcode
An Xcode project will now be available in the ``build`` subdirectory.
You can open it with Xcode, or run the following to build and install in
``/usr/local``:
.. code:: bash
xcodebuild
xcodebuild -scheme install
The install directory can be changed using the
``-DCMAKE_INSTALL_PREFIX`` parameter for ``cmake``.
Build options
~~~~~~~~~~~~~
In addition to the normal cmake build options (like ``CMAKE_INSTALL_PREFIX``), fish has some other options available to customize it.
In addition to the normal CMake build options (like ``CMAKE_INSTALL_PREFIX``), fish has some other options available to customize it.
- BUILD_DOCS=ON|OFF - whether to build the documentation. This is automatically set to OFF when sphinx isn't installed.
- BUILD_DOCS=ON|OFF - whether to build the documentation. This is automatically set to OFF when Sphinx isn't installed.
- INSTALL_DOCS=ON|OFF - whether to install the docs. This is automatically set to on when BUILD_DOCS is or prebuilt documentation is available (like when building in-tree from a tarball).
- FISH_USE_SYSTEM_PCRE2=ON|OFF - whether to use an installed pcre2. This is normally autodetected.
- MAC_CODESIGN_ID=String|OFF - the codesign ID to use on Mac, or "OFF" to disable codesigning.

View File

@@ -0,0 +1,12 @@
set -l compdir (status dirname)/../../share/completions
cd $compdir
for file in *.fish
set -l bname (string replace -r '.fish$' '' -- $file)
if type -q $bname
source $file >/dev/null
if test $status -gt 0
echo FAILING FILE $file
end
end
end

View File

@@ -0,0 +1,6 @@
set -l path (status dirname)
set -l fish (status fish-path)
for f in (seq 100)
echo $fish -n $path/aliases.fish
$fish -n $path/aliases.fish
end

View File

@@ -0,0 +1 @@
printf (string repeat -n 200 \\x7f)%s\n (string repeat -n 2000 aaa\n)

View File

@@ -0,0 +1,5 @@
for i in (seq 100000)
printf '%f\n' $i.$i
end
exit 0

View File

@@ -0,0 +1,7 @@
set -l tmp (mktemp)
string repeat -n 2000 >$tmp
for i in (seq 1000)
cat $tmp | read -l foo
end
true

View File

@@ -0,0 +1,3 @@
for abc in (seq 100000)
set -l def
end

View File

@@ -0,0 +1,3 @@
for i in (string repeat -n 100 \n)
string repeat -n 50000 a\n
end

View File

@@ -0,0 +1,3 @@
for i in (seq 100000)
string match '*o' fooooooo
end

View File

@@ -0,0 +1,3 @@
for i in (seq 100000)
string match -r '^.*$' fooooooo
end | string match -re o

View File

@@ -0,0 +1,3 @@
# Ignore a one-off leak in __cxa_thread_atexit_impl that isn't under our control.
# See https://github.com/fish-shell/fish-shell/pull/9754#issuecomment-1523782989
fun:__cxa_thread_atexit_impl

View File

@@ -39,7 +39,7 @@ end
function cleanup_syname
set -l symname $argv[1]
set symname (string replace --all 'std::__1::basic_string<wchar_t, std::__1::char_traits<wchar_t>, std::__1::allocator<wchar_t> >' 'wcstring' $symname)
set symname (string replace --all 'std::__1::vector<wcstring, std::__1::allocator<wcstring > >' 'wcstring_list_t' $symname)
set symname (string replace --all 'std::__1::vector<wcstring, std::__1::allocator<wcstring > >' 'std::vector<wcstring>' $symname)
echo $symname
end

View File

@@ -92,10 +92,7 @@
{ symbol: ["assert", "private", '"../common.h"', "public"] },
{ symbol: ["wcstring", "private", '"common.h"', "public"] },
{ symbol: ["wcstring", "private", '"../common.h"', "public"] },
{ symbol: ["wcstring_list_t", "private", '"common.h"', "public"] },
{ symbol: ["wcstring_list_t", "private", '"../common.h"', "public"] },
{ symbol: ["wcstring", "private", '"flog.h"', "public"] },
{ symbol: ["wcstring_list_t", "private", '"flog.h"', "public"] },
{ symbol: ["size_t", "private", "<cstddef>", "public"] },
{ symbol: ["mutex", "private", "<mutex>", "public"] },
{ symbol: ["sig_atomic_t", "private", "<csignal>", "public"] },

View File

@@ -0,0 +1,6 @@
# LSAN can detect leaks tracing back to __asan::AsanThread::ThreadStart (probably caused by our
# threads not exiting before their TLS dtors are called). Just ignore it.
leak:AsanThread
# ncurses leaks allocations freely as it assumes it will be running throughout. Ignore these.
leak:tparm

View File

@@ -14,6 +14,14 @@ 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
@@ -51,8 +59,8 @@ git archive --format=tar --prefix="$prefix"/ HEAD > "$path"
PREFIX_TMPDIR=$(mktemp -d)
cd "$PREFIX_TMPDIR"
echo "$VERSION" > version
cmake "$wd"
make doc
cmake -G "$BUILD_GENERATOR" "$wd"
$BUILD_TOOL doc
TAR_APPEND="$TAR --append --file=$path --mtime=now --owner=0 --group=0 \
--mode=g+w,a+rX --transform s/^/$prefix\//"
@@ -60,12 +68,17 @@ $TAR_APPEND --no-recursion user_doc
$TAR_APPEND user_doc/html user_doc/man
$TAR_APPEND version
if [ -n "$VENDOR_TARBALLS" ]; then
$BUILD_TOOL corrosion-vendor.tar.gz
mv corrosion-vendor.tar.gz ${FISH_ARTEFACT_PATH:-~/fish_built}/"${prefix}"_corrosion-vendor.tar.gz
fi
cd -
rm -r "$PREFIX_TMPDIR"
# xz it
xz "$path"
# Output what we did, and the sha1 hash
# Output what we did, and the sha256 hash
echo "Tarball written to $path".xz
openssl dgst -sha256 "$path".xz

View File

@@ -7,6 +7,7 @@ set -l git_clang_format no
set -l c_files
set -l fish_files
set -l python_files
set -l rust_files
set -l all no
if test "$argv[1]" = --all
@@ -25,13 +26,14 @@ if test $all = yes
echo
echo 'You have uncommitted changes. Are you sure you want to restyle?'
read -P 'y/N? ' -n1 -l ans
if not string match -qi "y" -- $ans
if not string match -qi y -- $ans
exit 1
end
end
set c_files src/*.h src/*.cpp src/*.c
set fish_files share/**.fish
set python_files {doc_src,share,tests}/**.py
set rust_files fish-rust/src/**.rs
else
# We haven't been asked to reformat all the source. If there are uncommitted changes reformat
# those using `git clang-format`. Else reformat the files in the most recent commit.
@@ -52,6 +54,7 @@ else
# Extract just the fish files.
set fish_files (string match -r '^.*\.fish$' -- $files)
set python_files (string match -r '^.*\.py$' -- $files)
set rust_files (string match -r '^.*\.rs$' -- $files)
end
set -l red (set_color red)
@@ -82,9 +85,9 @@ if set -q c_files[1]
if set -q c_files[1]
printf "Reformat those %d files?\n" (count $c_files)
read -P 'y/N? ' -n1 -l ans
if string match -qi "y" -- $ans
if string match -qi y -- $ans
clang-format -i --verbose $c_files
else if string match -qi "n" -- $ans
else if string match -qi n -- $ans
echo Skipping
else # like they ctrl-C'd or something.
exit 1
@@ -117,3 +120,14 @@ if set -q python_files[1]
black $python_files
end
end
if set -q rust_files[1]
if not type -q rustfmt
echo
echo Please install "`rustfmt`" to style rust
echo
else
echo === Running "$blue"rustfmt"$normal"
rustfmt $rust_files
end
end

View File

@@ -1,8 +1,6 @@
# The following defines affect the environment configuration tests are run in:
# CMAKE_REQUIRED_DEFINITIONS, CMAKE_REQUIRED_FLAGS, CMAKE_REQUIRED_LIBRARIES,
# and CMAKE_REQUIRED_INCLUDES
# `wcstod_l` is a GNU-extension, sometimes hidden behind GNU-related defines.
# This is the case for at least Cygwin and Newlib.
list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_GNU_SOURCE=1)
include(CheckCXXCompilerFlag)
include(CMakePushCheckState)
@@ -79,6 +77,12 @@ list(APPEND CMAKE_REQUIRED_INCLUDES ${CURSES_INCLUDE_DIRS})
find_library(CURSES_TINFO tinfo)
if (CURSES_TINFO)
set(CURSES_LIBRARY ${CURSES_LIBRARY} ${CURSES_TINFO})
else()
# on NetBSD, libtinfo has a longer name (libterminfo)
find_library(CURSES_TINFO terminfo)
if (CURSES_TINFO)
set(CURSES_LIBRARY ${CURSES_LIBRARY} ${CURSES_TINFO})
endif()
endif()
# Get threads.
@@ -161,16 +165,7 @@ if(NOT HAVE_WCSNCASECMP)
check_cxx_symbol_exists(std::wcsncasecmp wchar.h HAVE_STD__WCSNCASECMP)
endif()
# `xlocale.h` is required to find `wcstod_l` in `wchar.h` under FreeBSD,
# but it's not present under Linux.
check_include_files("xlocale.h" HAVE_XLOCALE_H)
if(HAVE_XLOCALE_H)
list(APPEND WCSTOD_L_INCLUDES "xlocale.h")
endif()
list(APPEND WCSTOD_L_INCLUDES "wchar.h")
check_cxx_symbol_exists(wcstod_l "${WCSTOD_L_INCLUDES}" HAVE_WCSTOD_L)
check_cxx_symbol_exists(uselocale "locale.h;xlocale.h" HAVE_USELOCALE)
cmake_push_check_state()
check_struct_has_member("struct winsize" ws_row "termios.h;sys/ioctl.h" _HAVE_WINSIZE)

View File

@@ -1,5 +1,3 @@
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.10" CACHE STRING "Minimum OS X deployment version")
# Code signing ID on Mac.
# If this is falsey, codesigning is disabled.
# '-' is ad-hoc codesign.

86
cmake/Rust.cmake Normal file
View File

@@ -0,0 +1,86 @@
if(EXISTS "${CMAKE_SOURCE_DIR}/corrosion-vendor/")
add_subdirectory("${CMAKE_SOURCE_DIR}/corrosion-vendor/")
else()
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)
add_custom_target(corrosion-vendor.tar.gz
COMMAND git archive --format tar.gz --output "${CMAKE_BINARY_DIR}/corrosion-vendor.tar.gz"
--prefix corrosion-vendor/ HEAD
WORKING_DIRECTORY ${corrosion_SOURCE_DIR}
)
endif()
set(fish_rust_target "fish-rust")
set(fish_autocxx_gen_dir "${CMAKE_BINARY_DIR}/fish-autocxx-gen/")
set(FISH_CRATE_FEATURES "fish-ffi-tests")
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")
list(APPEND FISH_CRATE_FEATURES "asan")
endif()
corrosion_import_crate(
MANIFEST_PATH "${CMAKE_SOURCE_DIR}/Cargo.toml"
CRATES "fish-rust"
FEATURES "${FISH_CRATE_FEATURES}"
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()
# Temporary hack to propogate CMake flags/options to build.rs. We need to get CMake to evaluate the
# truthiness of the strings if they are set.
set(CMAKE_WITH_GETTEXT "1")
if(DEFINED WITH_GETTEXT AND NOT "${WITH_GETTEXT}")
set(CMAKE_WITH_GETTEXT "0")
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}"
"PREFIX=${CMAKE_INSTALL_PREFIX}"
# Temporary hack to propogate CMake flags/options to build.rs.
"CMAKE_WITH_GETTEXT=${CMAKE_WITH_GETTEXT}"
)
target_include_directories(${fish_rust_target} INTERFACE
"${rust_target_dir}/cxxbridge/${fish_rust_target}/src/"
"${fish_autocxx_gen_dir}/include/"
)
# Tell fish what extra C++ files to compile.
define_property(
TARGET PROPERTY fish_extra_cpp_files
BRIEF_DOCS "Extra C++ files to compile for fish."
FULL_DOCS "Extra C++ files to compile for fish."
)
set_property(TARGET ${fish_rust_target} PROPERTY fish_extra_cpp_files
"${fish_autocxx_gen_dir}/cxx/gen0.cxx"
)

View File

@@ -175,3 +175,34 @@ 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} --package fish-rust --target-dir target ${cargo_target_opt}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
)
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} --package widestring-suffix --target-dir target ${cargo_target_opt}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
)
add_test_target("cargo-test-widestring")

View File

@@ -94,9 +94,6 @@
/* Define to 1 if you have the `wcsncasecmp' function. */
#cmakedefine HAVE_WCSNCASECMP 1
/* Define to 1 if you have the `wcstod_l' function. */
#cmakedefine HAVE_WCSTOD_L 1
/* Define to 1 if the status that wait returns and WEXITSTATUS expects is signal and then ret instead of the other way around. */
#cmakedefine HAVE_WAITSTATUS_SIGNAL_RET 1
@@ -144,9 +141,6 @@
/* Define if xlocale.h is required for locale_t or wide character support */
#cmakedefine HAVE_XLOCALE_H 1
/* Define if uselocale is available */
#cmakedefine HAVE_USELOCALE 1
/* Enable large inode numbers on Mac OS X 10.5. */
#ifndef _DARWIN_USE_64_BIT_INODE
# define _DARWIN_USE_64_BIT_INODE 1

2
debian/control vendored
View File

@@ -6,7 +6,7 @@ Uploaders: David Adam <zanchey@ucc.gu.uwa.edu.au>
# Debhelper should be bumped to >= 10 once Ubuntu Xenial is no longer supported
Build-Depends: debhelper (>= 9.20160115), libncurses5-dev, cmake (>= 3.5.0), gettext, libpcre2-dev,
# Test dependencies
locales-all, python3
locales-all, python3, rustc (>= 1.67) | rustc-mozilla (>= 1.67), cargo
Standards-Version: 4.1.5
Homepage: https://fishshell.com/
Vcs-Git: https://github.com/fish-shell/fish-shell.git

291
debian/copyright vendored
View File

@@ -1,101 +1,226 @@
This work was packaged for Debian by David Adam <zanchey@ucc.gu.uwa.edu.au>
on Thu, 14 Jun 2012 20:33:34 +0800, based on work by James Vega
<jamessan@jamessan.com>. Modifications from the downstream Debian maintainer,
Tristan Seligmann <mithrandi@debian.org>, have also been included.
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: fish
Upstream-Contact: corydoras@ridiculousfish.com
Source: https://fishshell.com/
It was downloaded from:
Files: *
Copyright: 2005-2009 Axel Liljencrantz <axel@liljencrantz.se>
2009-2023 fish-shell contributors
License: GPL-2
https://github.com/fish-shell/fish-shell
Files: cmake/CheckIncludeFiles.cmake
Copyright: 2000-2017 Kitware, Inc. and Contributors
License: BSD-3-clause
Upstream Authors:
Files: doc_src/python_docs_theme/*
Copyright: 2001-2017 Python Software Foundation
2020-2023 fish-shell contributors
License: Python
Axel Liljencrantz
ridiculous_fish
Files: share/tools/web_config/js/angular*.js
Copyright: 2010-2020 Google LLC
License: MIT
Copyright:
Files: share/tools/web_config/themes/Dracula.theme
Copyright: 2018 Dracula Team
License: MIT
Copyright (C) 2005-2008 Axel Liljencrantz
Copyright (C) 2011-2012 ridiculous_fish
Files: fish-rust/src/builtins/printf.rs
Copyright: 1990-2007 Free Software Foundation, Inc.
2022 fish-shell contributors
License: GPL-2+
License:
Files: src/env.cpp
Copyright: 2005-2009 Axel Liljencrantz <axel@liljencrantz.se>
2007 Nicholas Marriott <nicm@users.sourceforge.net>
2009-2023 fish-shell contributors
License: GPL-2 and OpenBSD
Copyright (C) 2005-2008 Axel Liljencrantz
Files: src/fallback.cpp
Copyright: 2001 The NetBSD Foundation, Inc.
2005-2009 Axel Liljencrantz <axel@liljencrantz.se>
2009-2023 fish-shell contributors
License: GPL-2 and NetBSD
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.
Files: src/utf8.c
Copyright: 2007 Alexey Vatchenko <av@bsdua.org>
License: ISC
This program 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 General Public License for more details.
Files: debian/*
Copyright: 2005-2009 James Vega <jamessan@jamessan.com>
2012 David Adam <zanchey@ucc.gu.uwa.edu.au>
2015 Tristan Seligmann <mithrandi@debian.org>
2019-2022 Mo Zhou <lumin@debian.org>
License: GPL-2
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
MA 02110-1301, USA.
License: BSD-3-clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
.
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
.
* Neither the name of Kitware, Inc. nor the names of Contributors
may be used to endorse or promote products derived from this
software without specific prior written permission.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
On Debian systems, the complete text of the GNU General
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
License: GPL-2
Most of fish is licensed under the GNU General Public License version 2, and
you can redistribute it and/or modify it under the terms of the GNU GPL as
published by the Free Software Foundation.
.
This program 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 General Public License for
more details.
.
On Debian systems, the complete text of the GNU General Public License can be
found in `/usr/share/common-licenses/GPL-2'.
Fish contains code from the PCRE2 library to support regular expressions. This
code, created by Philip Hazel, is distributed under the terms of the BSD
license. Copyright © 1997-2015 University of Cambridge.
License: GPL-2+
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2, or (at your option) any later version.
.
This program 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 General Public License for more
details.
.
You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
Street, Fifth Floor, Boston, MA 02110-1301, USA.
.
On Debian systems, the complete text of the GNU General Public License can be
found in `/usr/share/common-licenses/GPL-2'.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
License: ISC
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
License: MIT
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.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
License: NetBSD
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
.
THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
- Neither the name of the University of Cambridge nor the names of any
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
License: OpenBSD
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Fish also contains small amounts of code under the OpenBSD license, namely a
version of the function strlcpy, modified for use with wide character strings.
This code is copyrighted by Todd C. Miller (1998). It also contains code from
tmux, copyrighted by Nicholas Marriott <nicm@users.sourceforge.net> (2007), and
made available under an identical license.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Fish contains code from the glibc library, namely the wcstok function
in fallback.c. This code is licensed under the LGPL.
On Debian systems, the complete text of the GNU Lesser General
Public License can be found in `/usr/share/common-licenses/LGPL'.
The Debian packaging is:
Copyright (C) 2005 James Vega <jamessan@jamessan.com>
Copyright (C) 2012 David Adam <zanchey@ucc.gu.uwa.edu.au>
Copyright (C) 2015 Tristan Seligmann <mithrandi@debian.org>
and is licensed under the GPL version 2, see above.
License: Python
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
.
2. Subject to the terms and conditions of this License Agreement, PSF
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python alone
or in any derivative version, provided, however, that PSF's License
Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001,
2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012,
2013, 2014 Python Software Foundation; All Rights Reserved" are
retained in Python alone or in any derivative version prepared by
Licensee.
.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.

View File

@@ -0,0 +1,79 @@
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

192
doc_internal/rust-devel.md Normal file
View File

@@ -0,0 +1,192 @@
# 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::prelude::*;
// This imports wstr, the L! macro, WString, a ToWString trait that supplies .to_wstring() along with other things
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}
// also imported by the prelude
#[widestrs]
fn get_shell_name() -> &'static wstr {
"fish"L // equivalent to L!("fish")
}
```
#### The wchar prelude
We have a prelude to make working with these string types a whole lot more ergonomic. In particular `WExt` supplies the null-terminated-compatible `.char_at(usize)`,
and a whole lot more methods that makes porting C++ code easier. It is also preferred to use char-based-methods like `.char_count()` and `.slice_{from,to}()`
of the `WExt` trait over directly calling `.len()` and `[usize..]/[..usize]`, as that makes the code compatible with a potential future change to UTF8-strings.
```rust
pub(crate) mod prelude {
pub(crate) use crate::{
wchar::{wstr, IntoCharIter, WString, L},
wchar_ext::{ToWString, WExt},
wutil::{sprintf, wgettext, wgettext_fmt, wgettext_str},
};
pub(crate) use widestring_suffix::widestrs;
}
```
### Strings for FFI
`WString` and `&wstr` are the common strings used by Rust components. At the FII boundary there are some additional strings for interop. _All of these are temporary for the duration of the port._
- `CxxWString` is the Rust binding of `std::wstring`. It is the wide-string analog to [`CxxString`](https://cxx.rs/binding/cxxstring.html) and is [added in our fork of cxx](https://github.com/ridiculousfish/cxx/blob/fish/src/cxx_wstring.rs). This is useful for functions which return e.g. `const wcstring &`.
- `W0String` is renamed [U32CString](https://docs.rs/widestring/latest/widestring/ucstring/struct.U32CString.html). This is basically `WString` except it _is_ nul-terminated. This is useful for getting a nul-terminated `const wchar_t *` to pass to C++ implementations.
- `wcharz_t` is an annoying C++ struct which merely wraps a `const wchar_t *`, used for passing these pointers from C++ to Rust. We would prefer to use `const wchar_t *` directly but `autocxx` refuses to generate bindings for types such as `std::vector<const wchar_t *>` so we wrap it in this silly struct.
Note C++ `wchar_t`, Rust `char`, and `u32` are effectively interchangeable: you can cast pointers to them back and forth (except we check upon u32->char conversion). However be aware of which types are nul-terminated.
These types should be confined to the FFI modules, in particular `wchar_ffi`. They should not "leak" into other modules. See the `wchar_ffi` module.
### Format strings
Rust's builtin `std::fmt` modules do not accept runtime-provided format strings, so we mostly won't use them, except perhaps for FLOG / other non-translated text.
Instead we'll continue to use printf-style strings, with a Rust printf implementation.
### Vectors
See [`Vec`](https://cxx.rs/binding/vec.html) and [`CxxVector`](https://cxx.rs/binding/cxxvector.html).
In many cases, `autocxx` refuses to allow vectors of certain types. For example, autocxx supports `std::vector` and `std::shared_ptr` but NOT `std::vector<std::shared_ptr<...>>`. To work around this one can create a helper (pointer, length) struct. Example:
```cpp
struct RustFFIJobList {
std::shared_ptr<job_t> *jobs;
size_t count;
};
```
This is just a POD (plain old data) so autocxx can generate bindings for it. Then it is trivial to convert it to a Rust slice:
```
pub fn get_jobs(ffi_jobs: &ffi::RustFFIJobList) -> &[SharedPtr<job_t>] {
unsafe { slice::from_raw_parts(ffi_jobs.jobs, ffi_jobs.count) }
}
```
Another workaround is to define a struct that contains the shared pointer, and create a vector of that struct.
## Development Tooling
The [autocxx guidance](https://google.github.io/autocxx/workflow.html#how-can-i-see-what-bindings-autocxx-has-generated) is helpful:
1. Install cargo expand (`cargo install cargo-expand`). Then you can use `cargo expand` to see the generated Rust bindings for C++. In particular this is useful for seeing failed expansions for C++ types that autocxx cannot handle.
2. In rust-analyzer, enable Proc Macro and Proc Macro Attributes.
## FFI
The boundary between Rust and C++ is referred to as the Foreign Function Interface, or FFI.
`autocxx` and `cxx` both are designed for long-term interop: C++ and Rust coexisting for years. To this end, both emphasize safety: requiring lots of `unsafe`, `Pin`, etc.
fish plans to use them only temporarily, with a focus on getting things working. To this end, both cxx and autocxx have been forked to support fish:
1. Relax the requirement that all functions taking pointers are `unsafe` (this just added noise).
2. Add support for `wchar_t` as a recognized type, and `CxxWString` analogous to `CxxString`.
See the `Cargo.toml` file for the locations of the forks.

View File

@@ -49,6 +49,13 @@ Combining these features, it is possible to create custom syntaxes, where a regu
> abbr >> ~/.config/fish/config.fish
> abbr --erase (abbr --list)
Alternatively you can keep them in a separate :ref:`configuration file <configuration>` by doing something like the following::
> abbr > ~/.config/fish/conf.d/myabbrs.fish
This will save all your abbrevations in "myabbrs.fish", overwriting the whole file so it doesn't leave any duplicates,
or restore abbreviations you had erased.
Of course any functions will have to be saved separately, see :doc:`funcsave <funcsave>`.
"add" subcommand
--------------------

View File

@@ -17,10 +17,9 @@ Description
A background job is executed simultaneously with fish, and does not have access to the keyboard. If no job is specified, the last job to be used is put in the background. If ``PID`` is specified, the jobs containing the specified process IDs are put in the background.
For compatibility with other shells, job expansion syntax is supported for ``bg``. A PID of the format ``%1`` will be interpreted as the PID of job 1. Job numbers can be seen in the output of :doc:`jobs <jobs>`.
A PID of the format ``%n``, where n is an integer, will be interpreted as the PID of job number n. Job numbers can be seen in the output of :doc:`jobs <jobs>`.
When at least one of the arguments isn't a valid job specifier,
``bg`` will print an error without backgrounding anything.
When at least one of the arguments isn't a valid job specifier, ``bg`` will print an error without backgrounding anything.
When all arguments are valid job specifiers, ``bg`` will background all matching jobs that exist.
@@ -29,10 +28,20 @@ The **-h** or **--help** option displays help about using this command.
Example
-------
The typical use is to run something, stop it with ctrl-z, and then continue it in the background with bg::
> find / -name "*.js" >/tmp/jsfiles 2>/dev/null # oh no, this takes too long, let's press Ctrl-z!
fish: Job 1, 'find / -name "*.js" >/tmp/jsfil…' has stopped
> bg
Send job 1 'find / -name "*.js" >/tmp/jsfiles 2>/dev/null' to background
> # I can continue using this shell!
> # Eventually:
fish: Job 1, 'find / -name "*.js" >/tmp/jsfil…' has ended
``bg 123 456 789`` will background the jobs that contain processes 123, 456 and 789.
If only 123 and 789 exist, it will still background them and print an error about 456.
``bg 123 banana`` or ``bg banana 123`` will complain that "banana" is not a valid job specifier.
``bg %1`` will background job 1.
``bg %2`` will background job 2.

View File

@@ -23,26 +23,23 @@ It can add bindings if given a SEQUENCE of characters to bind to. These should b
For example, :kbd:`Alt`\ +\ :kbd:`W` can be written as ``\ew``, and :kbd:`Control`\ +\ :kbd:`X` (^X) can be written as ``\cx``. Note that Alt-based key bindings are case sensitive and Control-based key bindings are not. This is a constraint of text-based terminals, not ``fish``.
The generic key binding that matches if no other binding does can be set by specifying a ``SEQUENCE`` of the empty string (that is, ``''`` ). For most key bindings, it makes sense to bind this to the ``self-insert`` function (i.e. ``bind '' self-insert``). This will insert any keystrokes not specifically bound to into the editor. Non-printable characters are ignored by the editor, so this will not result in control sequences being inserted.
The generic key binding that matches if no other binding does can be set by specifying a ``SEQUENCE`` of the empty string (``''``). For most key bindings, it makes sense to bind this to the ``self-insert`` function (i.e. ``bind '' self-insert``). This will insert any keystrokes not specifically bound to into the editor. Non-printable characters are ignored by the editor, so this will not result in control sequences being inserted.
If the ``-k`` switch is used, the name of a key (such as 'down', 'up' or 'backspace') is used instead of a sequence. The names used are the same as the corresponding curses variables, but without the 'key\_' prefix. (See ``terminfo(5)`` for more information, or use ``bind --key-names`` for a list of all available named keys). Normally this will print an error if the current ``$TERM`` entry doesn't have a given key, unless the ``-s`` switch is given.
To find out what sequence a key combination sends, you can use :doc:`fish_key_reader <fish_key_reader>`.
``COMMAND`` can be any fish command, but it can also be one of a set of special input functions. These include functions for moving the cursor, operating on the kill-ring, performing tab completion, etc. Use ``bind --function-names`` for a complete list of these input functions.
When ``COMMAND`` is a shellscript command, it is a good practice to put the actual code into a :ref:`function <syntax-function>` and simply bind to the function name. This way it becomes significantly easier to test the function while editing, and the result is usually more readable as well.
``COMMAND`` can be any fish command, but it can also be one of a set of special input functions. These include functions for moving the cursor, operating on the kill-ring, performing tab completion, etc. Use ``bind --function-names`` or :ref:`see below <special-input-functions>` for a list of these input functions.
.. note::
Special input functions cannot be combined with ordinary shell script commands. The commands must be entirely a sequence of special input functions (from ``bind -f``) or all shell script commands (i.e., valid fish script). To run special input functions from regular fish script, use ``commandline -f`` (see also :doc:`commandline <commandline>`). If a script produces output, it should finish by calling ``commandline -f repaint`` to tell fish that a repaint is in order.
The commands must be entirely a sequence of special input functions (from ``bind -f``) or all shell script commands (i.e., valid fish script). To run special input functions from regular fish script, use ``commandline -f`` (see also :doc:`commandline <commandline>`). If a script produces output, it should finish by calling ``commandline -f repaint`` so that fish knows to redraw the prompt.
If no ``SEQUENCE`` is provided, all bindings (or just the bindings in the given ``MODE``) are printed. If ``SEQUENCE`` is provided but no ``COMMAND``, just the binding matching that sequence is printed.
To save custom key bindings, put the ``bind`` statements into :ref:`config.fish <configuration>`. Alternatively, fish also automatically executes a function called ``fish_user_key_bindings`` if it exists.
Key bindings may use "modes", which mimics Vi's modal input behavior. The default mode is "default". Every key binding applies to a single mode; you can specify which one with ``-M MODE``. If the key binding should change the mode, you can specify the new mode with ``-m NEW_MODE``. The mode can be viewed and changed via the ``$fish_bind_mode`` variable. If you want to change the mode from inside a fish function, use ``set fish_bind_mode MODE``.
To save custom key bindings, put the ``bind`` statements into :ref:`config.fish <configuration>`. Alternatively, fish also automatically executes a function called ``fish_user_key_bindings`` if it exists.
Options
-------
The following options are available:
@@ -87,6 +84,8 @@ The following options are available:
**-h** or **--help**
Displays help about using this command.
.. _special-input-functions:
Special input functions
-----------------------
The following special input functions are available:
@@ -199,6 +198,9 @@ The following special input functions are available:
``history-pager``
invoke the searchable pager on history (incremental search); or if the history pager is already active, search further backwards in time.
``history-pager-delete``
permanently delete the history item selected in the history pager
``history-search-backward``
search the history for the previous match

View File

@@ -15,6 +15,8 @@ Description
**command** forces the shell to execute the program *COMMANDNAME* and ignore any functions or builtins with the same name.
In ``command foo``, ``command`` is a keyword.
The following options are available:
**-a** or **--all**

View File

@@ -1,6 +1,6 @@
.. _cmd-complete:
complete - edit command specific tab-completions
complete - edit command-specific tab-completions
================================================
Synopsis
@@ -8,7 +8,7 @@ Synopsis
.. synopsis::
complete ((-c | --command) | (-p | --path)) COMMAND [OPTIONS]
complete ((-c | --command) | (-p | --path)) COMMAND [OPTIONS]
complete (-C | --do-complete) [--escape] STRING
Description
@@ -72,7 +72,7 @@ The following options are available:
**-h** or **--help**
Displays help about using this command.
Command specific tab-completions in ``fish`` are based on the notion of options and arguments. An option is a parameter which begins with a hyphen, such as ``-h``, ``-help`` or ``--help``. Arguments are parameters that do not begin with a hyphen. Fish recognizes three styles of options, the same styles as the GNU getopt library. These styles are:
Command-specific tab-completions in ``fish`` are based on the notion of options and arguments. An option is a parameter which begins with a hyphen, such as ``-h``, ``-help`` or ``--help``. Arguments are parameters that do not begin with a hyphen. Fish recognizes three styles of options, the same styles as the GNU getopt library. These styles are:
- Short options, like ``-a``. Short options are a single character long, are preceded by a single hyphen and can be grouped together (like ``-la``, which is equivalent to ``-l -a``). Option arguments may be specified by appending the option with the value (``-w32``), or, if ``--require-parameter`` is given, in the following parameter (``-w 32``).

View File

@@ -22,7 +22,9 @@ It is (by default) safe to use :program:`fish_add_path` in config.fish, or it ca
Components are normalized by :doc:`realpath <realpath>`. Trailing slashes are ignored and relative paths are made absolute (but symlinks are not resolved). If a component already exists, it is not added again and stays in the same place unless the ``--move`` switch is given.
Components are added in the order they are given, and they are prepended to the path unless ``--append`` is given (if $fish_user_paths is used, that means they are last in $fish_user_paths, which is itself prepended to :envvar:`PATH`, so they still stay ahead of the system paths).
Components are added in the order they are given, and they are prepended to the path unless ``--append`` is given. If $fish_user_paths is used, that means they are last in $fish_user_paths, which is itself prepended to :envvar:`PATH`, so they still stay ahead of the system paths. If the ``--path`` option is used, the paths are appended/prepended to :envvar:`PATH` directly, so this doesn't happen.
With ``--path``, because :envvar:`PATH` must be a global variable instead of a universal one, the changes won't persist, so those calls need to be stored in :ref:`config.fish <configuration>`.
If no component is new, the variable (:envvar:`fish_user_paths` or :envvar:`PATH`) is not set again or otherwise modified, so variable handlers are not triggered.
@@ -68,18 +70,26 @@ Example
::
# I just installed mycoolthing and need to add it to the path to use it.
# It is at /opt/mycoolthing/bin/mycoolthing,
# so let's add the directory: /opt/mycoolthing/bin.
> fish_add_path /opt/mycoolthing/bin
# I want my ~/.local/bin to be checked first.
# I want my ~/.local/bin to be checked first,
# even if it was already added.
> fish_add_path -m ~/.local/bin
# I prefer using a global fish_user_paths
# This isn't saved automatically, I need to add this to config.fish
# if I want it to stay.
> fish_add_path -g ~/.local/bin ~/.otherbin /usr/local/sbin
# I want to append to the entire $PATH because this directory contains fallbacks
> fish_add_path -aP /opt/fallback/bin
# This needs --path/-P because otherwise it appends to $fish_user_paths,
# which is added to the front of $PATH.
> fish_add_path --append --path /opt/fallback/bin
# I want to add the bin/ directory of my current $PWD (say /home/nemo/)
# -v/--verbose shows what fish_add_path did.
> fish_add_path -v bin/
set fish_user_paths /home/nemo/bin /usr/bin /home/nemo/.local/bin

View File

@@ -41,10 +41,46 @@ Available subcommands for the ``theme`` command:
- ``save`` saves the given theme to :ref:`universal variables <variables-universal>`.
- ``show`` shows what the given sample theme (or all) would look like.
The themes are loaded from the theme directory shipped with fish or a ``themes`` directory in the fish configuration directory (typically ``~/.config/fish/themes``).
The **-h** or **--help** option displays help about using this command.
Theme Files
-----------
``fish_config theme`` and the theme selector in the web config tool load their themes from theme files. These are stored in the fish configuration directory, typically ``~/.config/fish/themes``, with a .theme ending.
You can add your own theme by adding a file in that directory.
To get started quickly::
fish_config theme dump > ~/.config/fish/themes/my.theme
which will save your current theme in .theme format.
The format looks like this:
.. highlight:: none
::
# name: 'Cool Beans'
# preferred_background: black
fish_color_autosuggestion 666
fish_color_cancel -r
fish_color_command normal
fish_color_comment '888' '--italics'
fish_color_cwd 0A0
fish_color_cwd_root A00
fish_color_end 009900
The two comments at the beginning are the name and background that the web config tool shows.
The other lines are just like ``set variable value``, except that no expansions are allowed. Quotes are, but aren't necessary.
Any color variable fish knows about that the theme doesn't set will be set to empty when it is loaded, so the old theme is completely overwritten.
Other than that, .theme files can contain any variable with a name that matches the regular expression ``'^fish_(?:pager_)?color.*$'`` - starts with ``fish_``, an optional ``pager_``, then ``color`` and then anything.
Example
-------

View File

@@ -0,0 +1,27 @@
.. _cmd-fish_default_key_bindings:
fish_default_key_bindings - set emacs key bindings for fish
===============================================================
Synopsis
--------
.. synopsis::
fish_default_key_bindings
Description
-----------
``fish_default_key_bindings`` sets the emacs key bindings for ``fish`` shell.
Some of the Emacs key bindings are defined :ref:`here <emacs-mode>`.
There are no parameters for ``fish_default_key_bindings``.
Examples
--------
To start using vi key bindings::
fish_default_key_bindings

View File

@@ -56,7 +56,7 @@ Example
> fish_key_reader --verbose
Press a key:
# press alt+enter
hex: 1B char: \c[ (or \e)
hex: 1B char: \e
( 0.027 ms) hex: D char: \cM (or \r)
bind \e\r 'do something'

View File

@@ -0,0 +1,36 @@
.. _cmd-fish_vi_key_bindings:
fish_vi_key_bindings - set vi key bindings for fish
===============================================================
Synopsis
--------
.. synopsis::
fish_vi_key_bindings
fish_vi_key_bindings [--no-erase] [INIT_MODE]
Description
-----------
``fish_vi_key_bindings`` sets the vi key bindings for ``fish`` shell.
If a valid *INIT_MODE* is provided (insert, default, visual), then that mode will become the default
. If no *INIT_MODE* is given, the mode defaults to insert mode.
The following parameters are available:
**--no-erase**
Does not clear previous set bindings
Further information on how to use :ref:`vi-mode <vi-mode>`.
Examples
--------
To start using vi key bindings::
fish_vi_key_bindings
or ``set -g fish_key_bindings fish_vi_key_bindings`` in :ref:`config.fish <configuration>`.

View File

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

View File

@@ -29,7 +29,7 @@ The following operations (sub-commands) are available:
Returns history items matching the search string. If no search string is provided it returns all history items. This is the default operation if no other operation is specified. You only have to explicitly say ``history search`` if you wish to search for one of the subcommands. The ``--contains`` search option will be used if you don't specify a different search option. Entries are ordered newest to oldest unless you use the ``--reverse`` flag. If stdout is attached to a tty the output will be piped through your pager by the history function. The history builtin simply writes the results to stdout.
**delete**
Deletes history items. The ``--contains`` search option will be used if you don't specify a different search option. If you don't specify ``--exact`` a prompt will be displayed before any items are deleted asking you which entries are to be deleted. You can enter the word "all" to delete all matching entries. You can enter a single ID (the number in square brackets) to delete just that single entry. You can enter more than one ID separated by a space to delete multiple entries. Just press [enter] to not delete anything. Note that the interactive delete behavior is a feature of the history function. The history builtin only supports ``--exact --case-sensitive`` deletion.
Deletes history items. The ``--contains`` search option will be used if you don't specify a different search option. If you don't specify ``--exact`` a prompt will be displayed before any items are deleted asking you which entries are to be deleted. You can enter the word "all" to delete all matching entries. You can enter a single ID (the number in square brackets) to delete just that single entry. You can enter more than one ID, or an ID range separated by a space to delete multiple entries. Just press [enter] to not delete anything. Note that the interactive delete behavior is a feature of the history function. The history builtin only supports ``--exact --case-sensitive`` deletion.
**merge**
Immediately incorporates history changes from other sessions. Ordinarily ``fish`` ignores history changes from sessions started after the current one. This command applies those changes immediately.

View File

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

View File

@@ -17,8 +17,9 @@ Description
``math`` performs mathematical calculations.
It supports simple operations such as addition, subtraction, and so on, as well as functions like ``abs()``, ``sqrt()`` and ``ln()``.
By default, the output is a floating-point number with trailing zeroes trimmed.
To get a fixed representation, the ``--scale`` option can be used, including ``--scale=0`` for integer output.
By default, the output shows up to 6 decimal places.
To change the number of decimal places, use the ``--scale`` option, including ``--scale=0`` for integer output.
Trailing zeroes will always be trimmed.
Keep in mind that parameter expansion happens before expressions are evaluated.
This can be very useful in order to perform calculations involving shell variables or the output of command substitutions, but it also means that parenthesis (``()``) and the asterisk (``*``) glob character have to be escaped or quoted.

View File

@@ -365,19 +365,15 @@ Examples
>_ path change-extension '' ../banana
../banana
# but status 1, because there was no extension.
>_ path change-extension '' ~/.config
/home/alfa/.config
# status 1
>_ path change-extension '' ~/.config.d
/home/alfa/.config
# status 0
>_ path change-extension '' ~/.config.
/home/alfa/.config
# status 0
"sort" subcommand
-----------------------------

View File

@@ -106,7 +106,7 @@ The following code stores the value 'hello' in the shell variable :envvar:`foo`.
echo hello|read foo
While this is a neat way to handle command output line-by-line::
The :doc:`while <while>` command is a neat way to handle command output line-by-line::
printf '%s\n' line1 line2 line3 line4 | while read -l foo
echo "This is another line: $foo"

View File

@@ -66,7 +66,7 @@ These options modify how variables operate:
Treat specified variable as a :ref:`path variable <variables-path>`; variable will be split on colons (``:``) and will be displayed joined by colons colons when quoted (``echo "$PATH"``) or exported.
**--unpath**
Causes variable to no longer be tred as a :ref:`path variable <variables-path>`.
Causes variable to no longer be treated as a :ref:`path variable <variables-path>`.
Note: variables ending in "PATH" are automatically path variables.
Further options:
@@ -175,7 +175,7 @@ Remove _$smurf_ from the scope::
> set -e smurf
Remove _$smurf_ from the global and universal scoeps::
Remove _$smurf_ from the global and universal scopes::
> set -e -Ug smurf

View File

@@ -48,13 +48,13 @@ Examples
>_ string pad -w$COLUMNS (date)
# Prints the current time on the right edge of the screen.
.. END EXAMPLES
See Also
--------
.. BEGIN SEEALSO
- The :doc:`printf <printf>` command can do simple padding, for example ``printf %10s\n`` works like ``string pad -w10``.
- :doc:`string length <string-length>` with the ``--visible`` option can be used to show what fish thinks the width is.
.. END EXAMPLES

View File

@@ -81,13 +81,15 @@ Examples
# Taking 20 columns from the right instead:
…in-path-with-expand
.. END EXAMPLES
See Also
--------
.. BEGIN SEEALSO
- :ref:`string<cmd-string>`'s ``pad`` subcommand does the inverse of this command, adding padding to a specific width instead.
- The :doc:`printf <printf>` command can do simple padding, for example ``printf %10s\n`` works like ``string pad -w10``.
- :doc:`string length <string-length>` with the ``--visible`` option can be used to show what fish thinks the width is.
.. END EXAMPLES

View File

@@ -154,8 +154,8 @@ Examples
:start-after: BEGIN EXAMPLES
:end-before: END EXAMPLES
"pad" and "shorten" subcommands
---------------------------------
"pad" subcommand
----------------
.. include:: string-pad.rst
:start-after: BEGIN SYNOPSIS
@@ -165,10 +165,22 @@ Examples
:start-after: BEGIN DESCRIPTION
:end-before: END DESCRIPTION
Examples
^^^^^^^^
.. include:: string-pad.rst
:start-after: BEGIN EXAMPLES
:end-before: END EXAMPLES
See also
^^^^^^^^
.. include:: string-pad.rst
:start-after: BEGIN SEEALSO
"shorten" subcommand
--------------------
.. include:: string-shorten.rst
:start-after: BEGIN SYNOPSIS
:end-before: END SYNOPSIS
@@ -177,10 +189,19 @@ Examples
:start-after: BEGIN DESCRIPTION
:end-before: END DESCRIPTION
Examples
^^^^^^^^
.. include:: string-shorten.rst
:start-after: BEGIN EXAMPLES
:end-before: END EXAMPLES
See also
^^^^^^^^
.. include:: string-shorten.rst
:start-after: BEGIN SEEALSO
"repeat" subcommand
-------------------

View File

@@ -11,7 +11,6 @@ Synopsis
test [EXPRESSION]
[ [EXPRESSION] ]
Description
-----------
@@ -21,13 +20,13 @@ Description
To see the documentation on the ``test`` command you might have,
use ``command man test``.
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.
``test`` checks the given conditions and sets the exit status to 0 if they are true, 1 if they are false.
The first form (``test``) is preferred. For compatibility with other shells, the second form is available: a matching pair of square brackets (``[ [EXPRESSION] ]``).
This test is mostly POSIX-compatible.
When using a variable or command substitution 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``.
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.
For historical reasons, ``test`` supports the one-argument form (``test foo``), and this will also be triggered by e.g. ``test -n $foo`` if $foo is unset. We recommend you don't use the one-argument form and quote all variables or command substitutions used with ``test``.
Operators for files and directories
-----------------------------------
@@ -155,7 +154,7 @@ Expressions can be grouped using parentheses.
**(** *EXPRESSION* **)**
Returns the value of *EXPRESSION*.
Note that parentheses will usually require escaping with ``\(`` to avoid being interpreted as a command substitution.
Note that parentheses will usually require escaping with ``\`` (so they appear as ``\(`` and ``\)``) to avoid being interpreted as a command substitution.
Examples
@@ -163,8 +162,6 @@ Examples
If the ``/tmp`` directory exists, copy the ``/etc/motd`` file to it:
::
if test -d /tmp
@@ -174,19 +171,22 @@ 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"
echo $MANPATH
end
Be careful with unquoted variables::
if test -n $MANPATH
# This will also be reached if $MANPATH is unset,
# because in that case we have `test -n`, so it checks if "-n" is non-empty, and it is.
echo $MANPATH
end
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 \)
@@ -196,30 +196,22 @@ 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
@@ -229,8 +221,6 @@ The previous test can likewise be inverted:
which is logically equivalent to the following:
::
if test $status -ne 0
@@ -241,10 +231,16 @@ which is logically equivalent to the following:
Standards
---------
``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:
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:
- The ``<`` and ``>`` operators for comparing strings are not implemented.
- Because this test is a shell builtin and not a standalone utility, using the -c flag on a special file descriptors like standard input and output may not return the same result when invoked from within a pipe as one would expect when invoking the ``test`` utility in another shell.
In cases such as this, one can use ``command`` ``test`` to explicitly use the system's standalone ``test`` rather than this ``builtin`` ``test``.
See also
--------
Other commands that may be useful as a condition, and are often easier to use:
- :doc:`string`, which can do string operations including wildcard and regular expression matching
- :doc:`path`, which can do file checks and operations, including filters on multiple paths at once

View File

@@ -21,10 +21,10 @@ The following options are available:
Prints all of possible definitions of the specified names.
**-s** or **--short**
Suppresses function expansion when used with no options or with **-a**/**--all**.
Don't print function definitions when used with no options or with **-a**/**--all**.
**-f** or **--no-functions**
Suppresses function and builtin lookup.
Suppresses function lookup.
**-t** or **--type**
Prints ``function``, ``builtin``, or ``file`` if *NAME* is a shell function, builtin, or disk file, respectively.

View File

@@ -35,6 +35,7 @@ Builtins to do a task, like
- :doc:`set <cmds/set>` to set, query or erase variables.
- :doc:`read <cmds/read>` to read input.
- :doc:`string <cmds/string>` for string manipulation.
- :doc:`path <cmds/path>` for filtering paths and handling their components.
- :doc:`math <cmds/math>` does arithmetic.
- :doc:`argparse <cmds/argparse>` to make arguments easier to handle.
- :doc:`count <cmds/count>` to count arguments.

View File

@@ -5,14 +5,57 @@ Writing your own completions
To specify a completion, use the ``complete`` command. ``complete`` takes as a parameter the name of the command to specify a completion for. For example, to add a completion for the program ``myprog``, one would start the completion command with ``complete -c myprog ...``
To provide a list of possible completions for myprog, use the ``-a`` switch. If ``myprog`` accepts the arguments start and stop, this can be specified as ``complete -c myprog -a 'start stop'``. The argument to the ``-a`` switch is always a single string. At completion time, it will be tokenized on spaces and tabs, and variable expansion, command substitution and other forms of parameter expansion will take place.
To provide a list of possible completions for myprog, use the ``-a`` switch. If ``myprog`` accepts the arguments start and stop, this can be specified as ``complete -c myprog -a 'start stop'``. The argument to the ``-a`` switch is always a single string. At completion time, it will be tokenized on spaces and tabs, and variable expansion, command substitution and other forms of parameter expansion will take place::
# If myprog can list the valid outputs with the list-outputs subcommand:
complete -c myprog -l output -a '(myprog list-outputs)'
``fish`` has a special syntax to support specifying switches accepted by a command. The switches ``-s``, ``-l`` and ``-o`` are used to specify a short switch (single character, such as ``-l``), a gnu style long switch (such as ``--color``) and an old-style long switch (like ``-shuffle``), respectively. If the command 'myprog' has an option '-o' which can also be written as ``--output``, and which can take an additional value of either 'yes' or 'no', this can be specified by writing::
complete -c myprog -s o -l output -a "yes no"
For a complete description of the various switches accepted by the ``complete`` command, see the documentation for the :doc:`complete <cmds/complete>` builtin, or write ``complete --help`` inside the ``fish`` shell.
There are also special switches for specifying that a switch requires an argument, to disable filename completion, to create completions that are only available in some combinations, etc.. For a complete description of the various switches accepted by the ``complete`` command, see the documentation for the :doc:`complete <cmds/complete>` builtin, or write ``complete --help`` inside the ``fish`` shell.
In the complete call above, the ``-a`` arguments apply when the option -o/--output has been given, so this offers them for::
> myprog -o<TAB>
> myprog --output=<TAB>
By default, option arguments are *optional*, so the candidates are only offered directly attached like that, so they aren't given in this case::
> myprog -o <TAB>
Usually options *require* a parameter, so you would give ``--require-parameter`` / ``-r``::
complete -c myprog -s o -l output -ra "yes no"
which offers yes/no in these cases::
> myprog -o<TAB>
> myprog --output=<TAB>
> myprog -o <TAB>
> myprog --output <TAB>
In the latter two cases, files will also be offered because file completion is enabled by default.
You would either inhibit file completion for a single option::
complete -c myprog -s o -l output --no-files -ra "yes no"
or with a specific condition::
complete -c myprog -f --condition '__fish_seen_subcommand_from somesubcommand'
or you can disable file completions globally for the command::
complete -c myprog -f
If you have disabled them globally, you can enable them just for a specific condition or option with the ``--force-files`` / ``-F`` option::
# Disable files by default
complete -c myprog -f
# but reenable them for --config-file
complete -c myprog -l config-file --force-files -r
As a more comprehensive example, here's a commented excerpt of the completions for systemd's ``timedatectl``::
@@ -38,7 +81,7 @@ As a more comprehensive example, here's a commented excerpt of the completions f
# The `-n`/`--condition` option takes script as a string, which it executes.
# If it returns true, the completion is offered.
# Here the condition is the `__fish_seen_subcommands_from` helper function.
# If returns true if any of the given commands is used on the commandline,
# It returns true if any of the given commands is used on the commandline,
# as determined by a simple heuristic.
# For more complex uses, you can write your own function.
# See e.g. the git completions for an example.
@@ -76,7 +119,7 @@ For examples of how to write your own complex completions, study the completions
Useful functions for writing completions
----------------------------------------
``fish`` ships with several functions that are very useful when writing command specific completions. Most of these functions name begins with the string ``__fish_``. Such functions are internal to ``fish`` and their name and interface may change in future fish versions. Still, some of them may be very useful when writing completions. A few of these functions are described here. Be aware that they may be removed or changed in future versions of fish.
``fish`` ships with several functions that may be useful when writing command-specific completions. Most of these function names begin with the string ``__fish_``. Such functions are internal to ``fish`` and their name and interface may change in future fish versions. A few of these functions are described here.
Functions beginning with the string ``__fish_print_`` print a newline separated list of strings. For example, ``__fish_print_filesystems`` prints a list of all known file systems. Functions beginning with ``__fish_complete_`` print out a newline separated list of completions with descriptions. The description is separated from the completion by a tab character.
@@ -98,8 +141,6 @@ Functions beginning with the string ``__fish_print_`` print a newline separated
- ``__fish_print_interfaces`` prints a list of all known network interfaces.
- ``__fish_print_packages`` prints a list of all installed packages. This function currently handles Debian, rpm and Gentoo packages.
.. _completion-path:
Where to put completions
@@ -120,7 +161,7 @@ These paths are controlled by parameters set at build, install, or run time, and
This wide search may be confusing. If you are unsure, your completions probably belong in ``~/.config/fish/completions``.
If you have written new completions for a common Unix command, please consider sharing your work by submitting it via the instructions in :ref:`Further help and development <more-help>`
If you have written new completions for a common Unix command, please consider sharing your work by submitting it via the instructions in :ref:`Further help and development <more-help>`.
If you are developing another program and would like to ship completions with your program, install them to the "vendor" completions directory. As this path may vary from system to system, the ``pkgconfig`` framework should be used to discover this path with the output of ``pkg-config --variable completionsdir fish``.

View File

@@ -113,7 +113,7 @@ html_theme_path = ["."]
html_theme = "python_docs_theme"
# Shared styles across all doc versions.
html_css_files = ["/docs/shared/style.css"]
html_css_files = []
# Don't add a weird "_sources" directory
html_copy_source = False
@@ -187,6 +187,7 @@ man_pages = [
("interactive", "fish-interactive", "", [author], 1),
("relnotes", "fish-releasenotes", "", [author], 1),
("completions", "fish-completions", "", [author], 1),
("prompt", "fish-prompt-tutorial", "", [author], 1),
(
"fish_for_bash_users",
"fish-for-bash-users",

1
doc_src/contributing.rst Normal file
View File

@@ -0,0 +1 @@
.. include:: ../CONTRIBUTING.rst

View File

@@ -166,18 +166,21 @@ As a special case, most of the time history substitution is used as ``sudo !!``.
In general, fish's history recall works like this:
- Like other shells, the Up arrow, :kbd:`↑` recalls whole lines, starting from the last executed line. A single press replaces "!!", later presses replace "!-3" and the like.
- Like other shells, the Up arrow, :kbd:`↑` recalls whole lines, starting from the last executed line. So instead of typing ``!!``, you would just hit the up-arrow.
- If the line you want is far back in the history, type any part of the line and then press Up one or more times. This will filter the recalled lines to ones that include this text, and you will get to the line you want much faster. This replaces "!vi", "!?bar.c" and the like.
- If the line you want is far back in the history, type any part of the line and then press Up one or more times. This will filter the recalled lines to ones that include this text, and you will get to the line you want much faster. This replaces "!vi", "!?bar.c" and the like. If you want to see more context, you can press :kbd:`Ctrl`\ +\ :kbd:`R` to open the history in the pager.
- :kbd:`Alt`\ +\ :kbd:`↑` recalls individual arguments, starting from the last argument in the last executed line. A single press replaces "!$", later presses replace "!!:4" and such. As an alternate key binding, :kbd:`Alt`\ +\ :kbd:`.` can be used.
- If the argument you want is far back in history (e.g. 2 lines back - that's a lot of words!), type any part of it and then press :kbd:`Alt`\ +\ :kbd:`↑`. This will show only arguments containing that part and you will get what you want much faster. Try it out, this is very convenient!
- If you want to reuse several arguments from the same line ("!!:3*" and the like), consider recalling the whole line and removing what you don't need (:kbd:`Alt`\ +\ :kbd:`D` and :kbd:`Alt`\ +\ :kbd:`Backspace` are your friends).
- :kbd:`Alt`\ +\ :kbd:`↑` recalls individual arguments, starting from the last argument in the last executed line. This can be used instead of "!$".
See :ref:`documentation <editor>` for more details about line editing in fish.
That being said, you can use :ref:`abbreviations` to implement history substitution. Here's just ``!!``::
function last_history_item; echo $history[1]; end
abbr -a !! --position anywhere --function last_history_item
Run this and ``!!`` will be replaced with the last history entry, anywhere on the commandline. Put it into :ref:`config.fish <configuration>` to keep it.
How do I run a subcommand? The backtick doesn't work!
-----------------------------------------------------
``fish`` uses parentheses for subcommands. For example::
@@ -294,14 +297,6 @@ For these reasons, fish does not do this, and instead expects asterisks to be qu
This is similar to bash's "failglob" option.
I accidentally entered a directory path and fish changed directory. What happened?
----------------------------------------------------------------------------------
If fish is unable to locate a command with a given name, and it starts with ``.``, ``/`` or ``~``, fish will test if a directory of that name exists. If it does, it assumes that you want to change your directory. For example, the fastest way to switch to your home directory is to simply press ``~`` and enter.
The open command doesn't work.
------------------------------
The ``open`` command uses the MIME type database and the ``.desktop`` files used by Gnome and KDE to identify filetypes and default actions. If at least one of these environments is installed, but the open command is not working, this probably means that the relevant files are installed in a non-standard location. Consider :ref:`asking for more help <more-help>`.
.. _faq-ssh-interactive:
Why won't SSH/SCP/rsync connect properly when fish is my login shell?
@@ -357,14 +352,3 @@ If you installed it with a package manager, just use that package manager's unin
rm /usr/local/share/man/man1/fish*.1
cd /usr/local/bin
rm -f fish fish_indent
Where can I find extra tools for fish?
--------------------------------------
The fish user community extends fish in unique and useful ways via scripts that aren't always appropriate for bundling with the fish package. Typically because they solve a niche problem unlikely to appeal to a broad audience. You can find those extensions, including prompts, themes and useful functions, in various third-party repositories. These include:
- `Fisher <https://github.com/jorgebucaran/fisher>`_
- `Fundle <https://github.com/tuvistavie/fundle>`_
- `Oh My Fish <https://github.com/oh-my-fish/oh-my-fish>`_
- `Tacklebox <https://github.com/justinmayer/tacklebox>`_
This is not an exhaustive list and the fish project has no opinion regarding the merits of the repositories listed above or the scripts found therein.

View File

@@ -60,7 +60,8 @@ And here is fish::
> set foo "bar baz"
> printf '"%s"\n' $foo
# foo was set as one element, so it will be passed as one element, so this is one line
# foo was set as one element,
# so it will be passed as one element, so this is one line
"bar baz"
All variables are "arrays" (we use the term "lists"), and expanding a variable expands to all its elements, with each element as its own argument (like bash's ``"${var[@]}"``::

View File

@@ -28,7 +28,7 @@ class FishSynopsisDirective(CodeBlock):
return CodeBlock.run(self)
lexer = FishSynopsisLexer()
result = nodes.line_block()
for (start, tok, text) in lexer.get_tokens_unprocessed("\n".join(self.content)):
for start, tok, text in lexer.get_tokens_unprocessed("\n".join(self.content)):
if ( # Literal text.
(tok in (Name.Function, Name.Constant) and not text.isupper())
or text.startswith("-") # Literal option, even if it's uppercase.

View File

@@ -25,9 +25,9 @@ If this is your first time using fish, see the :ref:`tutorial <tutorial>`.
If you are already familiar with other shells like bash and want to see the scripting differences, see :ref:`Fish For Bash Users <fish_for_bash_users>`.
For a comprehensive overview of fish's scripting language, see :ref:`The Fish Language <language>`.
For an overview of fish's scripting language, see :ref:`The Fish Language <language>`. If it would be useful in a script file, it's here.
For information on using fish interactively, see :ref:`Interactive use <interactive>`.
For information on using fish interactively, see :ref:`Interactive use <interactive>`. If it's about key presses, syntax highlighting or anything else that needs an interactive terminal session, look here.
If you need to install fish first, read on, the rest of this document will tell you how to get, install and configure fish.
@@ -107,11 +107,11 @@ If you want to share your script with others, you might want to use :command:`en
#!/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 /usr/local/bin/fish or /usr/bin/fish or ~/.local/bin/fish, as long as that directory is in :envvar:`PATH`.
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.
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
=============
@@ -171,6 +171,8 @@ Other help pages
fish_for_bash_users
tutorial
completions
prompt
design
relnotes
contributing
license

View File

@@ -125,6 +125,7 @@ Variable Meaning
.. envvar:: fish_color_status the last command's nonzero exit code in the default prompt
.. envvar:: fish_color_cancel the '^C' indicator on a canceled command
.. envvar:: fish_color_search_match history search matches and selected pager items (background only)
.. envvar:: fish_color_history_current the current position in the history for commands like ``dirh`` and ``cdh``
========================================== =====================================================================
@@ -189,7 +190,7 @@ To avoid needless typing, a frequently-run command like ``git checkout`` can be
After entering ``gco`` and pressing :kbd:`Space` or :kbd:`Enter`, a ``gco`` in command position will turn into ``git checkout`` in the command line. If you want to use a literal ``gco`` sometimes, use :kbd:`Control`\ +\ :kbd:`Space` [#]_.
This is a lot more powerful, for example you can make going up a number of directories easier with this::
Abbreviations are a lot more powerful than just replacing literal strings. For example you can make going up a number of directories easier with this::
function multicd
echo cd (string repeat -n (math (string length -- $argv[1]) - 1) ../)
@@ -202,40 +203,34 @@ The advantage over aliases is that you can see the actual command before using i
.. [#] Any binding that executes the ``expand-abbr`` or ``execute`` :doc:`bind function <cmds/bind>` will expand abbreviations. By default :kbd:`Control`\ +\ :kbd:`Space` is bound to just inserting a space.
.. _title:
Programmable title
------------------
When using most virtual terminals, it is possible to set the message displayed in the titlebar of the terminal window. This can be done automatically in fish by defining the :doc:`fish_title <cmds/fish_title>` function. The :doc:`fish_title <cmds/fish_title>` function is executed before and after a new command is executed or put into the foreground and the output is used as a titlebar message. The :doc:`status current-command <cmds/status>` builtin will always return the name of the job to be put into the foreground (or ``fish`` if control is returning to the shell) when the :doc:`fish_prompt <cmds/fish_prompt>` function is called. The first argument to fish_title will contain the most recently executed foreground command as a string.
The default fish title shows the hostname if connected via ssh, the currently running command (unless it is fish) and the current working directory. All of this is shortened to not make the tab too wide.
Examples:
To show the last command and working directory in the title::
function fish_title
# `prompt_pwd` shortens the title. This helps prevent tabs from becoming very wide.
echo $argv[1] (prompt_pwd)
pwd
end
.. _prompt:
Programmable prompt
-------------------
When it is fish's turn to ask for input (like after it started or the command ended), it will show a prompt. It does this by running the :doc:`fish_prompt <cmds/fish_prompt>` and :doc:`fish_right_prompt <cmds/fish_right_prompt>` functions.
When it is fish's turn to ask for input (like after it started or the command ended), it will show a prompt. Often this looks something like::
The output of the former is displayed on the left and the latter's output on the right side of the terminal. The output of :doc:`fish_mode_prompt <cmds/fish_mode_prompt>` will be prepended on the left, though the default function only does this when in :ref:`vi-mode <vi-mode>`.
you@hostname ~>
This prompt is determined by running the :doc:`fish_prompt <cmds/fish_prompt>` and :doc:`fish_right_prompt <cmds/fish_right_prompt>` functions.
The output of the former is displayed on the left and the latter's output on the right side of the terminal.
For :ref:`vi-mode <vi-mode>`, the output of :doc:`fish_mode_prompt <cmds/fish_mode_prompt>` will be prepended on the left.
Fish ships with a few prompts which you can see with :doc:`fish_config <cmds/fish_config>`. If you run just ``fish_config`` it will open a web interface [#]_ where you'll be shown the prompts and can pick which one you want. ``fish_config prompt show`` will show you the prompts right in your terminal.
For example ``fish_config prompt choose disco`` will temporarily select the "disco" prompt. If you like it and decide to keep it, run ``fish_config prompt save``.
You can also change these functions yourself by running ``funced fish_prompt`` and ``funcsave fish_prompt`` once you are happy with the result (or ``fish_right_prompt`` if you want to change that).
.. [#] The web interface runs purely locally on your computer and requires python to be installed.
.. _greeting:
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>`::
When it is started interactively, fish tries to run the :doc:`fish_greeting <cmds/fish_greeting>` function. The default fish_greeting prints a simple message. You can change its text by changing the ``$fish_greeting`` variable, for instance using a :ref:`universal variable <variables-universal>`::
set -U fish_greeting
@@ -251,16 +246,26 @@ or you can script it by changing the function::
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.
.. _private-mode:
.. _title:
Private mode
-------------
Programmable title
------------------
If ``$fish_private_mode`` is set to a non-empty value, commands will not be written to the history file on disk.
When using most terminals, it is possible to set the text displayed in the titlebar of the terminal window. Fish does this by running the :doc:`fish_title <cmds/fish_title>` function. It is executed before and after a command and the output is used as a titlebar message.
You can also launch with ``fish --private`` (or ``fish -P`` for short). This both hides old history and prevents writing history to disk. This is useful to avoid leaking personal information (e.g. for screencasts) or when dealing with sensitive information.
The :doc:`status current-command <cmds/status>` builtin will always return the name of the job to be put into the foreground (or ``fish`` if control is returning to the shell) when the :doc:`fish_title <cmds/fish_title>` function is called. The first argument will contain the most recently executed foreground command as a string.
You can query the variable ``fish_private_mode`` (``if test -n "$fish_private_mode" ...``) if you would like to respect the user's wish for privacy and alter the behavior of your own fish scripts.
The default title shows the hostname if connected via ssh, the currently running command (unless it is fish) and the current working directory. All of this is shortened to not make the tab too wide.
Examples:
To show the last command and working directory in the title::
function fish_title
# `prompt_pwd` shortens the title. This helps prevent tabs from becoming very wide.
echo $argv[1] (prompt_pwd)
pwd
end
.. _editor:
@@ -269,7 +274,7 @@ Command line editor
The fish editor features copy and paste, a :ref:`searchable history <history-search>` and many editor functions that can be bound to special keyboard shortcuts.
Like bash and other shells, fish includes two sets of keyboard shortcuts (or key bindings): one inspired by the Emacs text editor, and one by the Vi text editor. The default editing mode is Emacs. You can switch to Vi mode by running ``fish_vi_key_bindings`` and switch back with ``fish_default_key_bindings``. You can also make your own key bindings by creating a function and setting the ``fish_key_bindings`` variable to its name. For example::
Like bash and other shells, fish includes two sets of keyboard shortcuts (or key bindings): one inspired by the Emacs text editor, and one by the Vi text editor. The default editing mode is Emacs. You can switch to Vi mode by running :doc:`fish_vi_key_bindings <cmds/fish_vi_key_bindings>` and switch back with :doc:`fish_default_key_bindings <cmds/fish_default_key_bindings>`. You can also make your own key bindings by creating a function and setting the ``fish_key_bindings`` variable to its name. For example::
function fish_hybrid_key_bindings --description \
@@ -296,7 +301,7 @@ Some bindings are common across Emacs and Vi mode, because they aren't text edit
- :kbd:`Enter` executes the current commandline or inserts a newline if it's not complete yet (e.g. a ``)`` or ``end`` is missing).
- :kbd:`Alt`\ +\ :kbd:`Enter` inserts a newline at the cursor position.
- :kbd:`Alt`\ +\ :kbd:`Enter` inserts a newline at the cursor position. This is useful to add a line to a commandline that's already complete.
- :kbd:`Alt`\ +\ :kbd:`←` and :kbd:`Alt`\ +\ :kbd:`→` move the cursor one word left or right (to the next space or punctuation mark), or moves forward/backward in the directory history if the command line is empty. If the cursor is already at the end of the line, and an autosuggestion is available, :kbd:`Alt`\ +\ :kbd:`→` (or :kbd:`Alt`\ +\ :kbd:`F`) accepts the first word in the suggestion.
@@ -308,9 +313,9 @@ Some bindings are common across Emacs and Vi mode, because they aren't text edit
- :kbd:`Alt`\ +\ :kbd:`↑` and :kbd:`Alt`\ +\ :kbd:`↓` search the command history for the previous/next token containing the token under the cursor before the search was started. If the commandline was not on a token when the search started, all tokens match. See the :ref:`history <history-search>` section for more information on history searching.
- :kbd:`Control`\ +\ :kbd:`C` interrupt/kill whatever is running (SIGINT).
- :kbd:`Control`\ +\ :kbd:`C` interrupts/kills whatever is running (SIGINT).
- :kbd:`Control`\ +\ :kbd:`D` delete one character to the right of the cursor. If the command line is empty, :kbd:`Control`\ +\ :kbd:`D` will exit fish.
- :kbd:`Control`\ +\ :kbd:`D` deletes one character to the right of the cursor. If the command line is empty, :kbd:`Control`\ +\ :kbd:`D` will exit fish.
- :kbd:`Control`\ +\ :kbd:`U` removes contents from the beginning of line to the cursor (moving it to the :ref:`killring <killring>`).
@@ -332,7 +337,7 @@ Some bindings are common across Emacs and Vi mode, because they aren't text edit
- :kbd:`Alt`\ +\ :kbd:`W` prints a short description of the command under the cursor.
- :kbd:`Alt`\ +\ :kbd:`E` edit the current command line in an external editor. The editor is chosen from the first available of the ``$VISUAL`` or ``$EDITOR`` variables.
- :kbd:`Alt`\ +\ :kbd:`E` edits the current command line in an external editor. The editor is chosen from the first available of the ``$VISUAL`` or ``$EDITOR`` variables.
- :kbd:`Alt`\ +\ :kbd:`V` Same as :kbd:`Alt`\ +\ :kbd:`E`.
@@ -345,7 +350,7 @@ Some bindings are common across Emacs and Vi mode, because they aren't text edit
Emacs mode commands
^^^^^^^^^^^^^^^^^^^
To enable emacs mode, use ``fish_default_key_bindings``. This is also the default.
To enable emacs mode, use :doc:`fish_default_key_bindings <cmds/fish_default_key_bindings>`. This is also the default.
- :kbd:`Home` or :kbd:`Control`\ +\ :kbd:`A` moves the cursor to the beginning of the line.
@@ -390,8 +395,7 @@ Vi mode commands
Vi mode allows for the use of Vi-like commands at the prompt. Initially, :ref:`insert mode <vi-mode-insert>` is active. :kbd:`Escape` enters :ref:`command mode <vi-mode-command>`. The commands available in command, insert and visual mode are described below. Vi mode shares :ref:`some bindings <shared-binds>` with :ref:`Emacs mode <emacs-mode>`.
To enable vi mode, use ``fish_vi_key_bindings``.
To enable vi mode, use :doc:`fish_vi_key_bindings <cmds/fish_vi_key_bindings>`.
It is also possible to add all emacs-mode bindings to vi-mode by using something like::
@@ -418,14 +422,20 @@ The ``fish_vi_cursor`` function will be used to change the cursor's shape depend
set fish_cursor_default block
# Set the insert mode cursor to a line
set fish_cursor_insert line
# Set the replace mode cursor to an underscore
# Set the replace mode cursors to an underscore
set fish_cursor_replace_one underscore
set fish_cursor_replace 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
Additionally, ``blink`` can be added after each of the cursor shape parameters to set a blinking cursor in the specified shape.
Fish knows the shapes "block", "line" and "underscore", other values will be ignored.
If the cursor shape does not appear to be changing after setting the above variables, it's likely your terminal emulator does not support the capabilities necessary to do this. It may also be the case, however, that ``fish_vi_cursor`` has not detected your terminal's features correctly (for example, if you are using ``tmux``). If this is the case, you can force ``fish_vi_cursor`` to set the cursor shape by setting ``$fish_vi_force_cursor`` in ``config.fish``. You'll have to restart fish for any changes to take effect. If cursor shape setting remains broken after this, it's almost certainly an issue with your terminal emulator, and not fish.
.. _vi-mode-command:
@@ -443,7 +453,7 @@ Command mode is also known as normal mode.
- :kbd:`i` enters :ref:`insert mode <vi-mode-insert>` at the current cursor position.
- :kbd:`Shift`\ +\ :kbd:`R` enters :ref:`insert mode <vi-mode-insert>` at the beginning of the line.
- :kbd:`Shift`\ +\ :kbd:`I` enters :ref:`insert mode <vi-mode-insert>` at the beginning of the line.
- :kbd:`v` enters :ref:`visual mode <vi-mode-visual>` at the current cursor position.
@@ -534,6 +544,10 @@ If you change your mind on a binding and want to go back to fish's default, you
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.
If you use :ref:`vi bindings <vi-mode>`, note that ``bind`` will by default bind keys in :ref:`command mode <vi-mode-command>`. To bind something in :ref:`insert mode <vi-mode-insert>`::
bind --mode insert \cc 'commandline -r ""'
Key sequences
"""""""""""""
@@ -620,6 +634,17 @@ If the commandline reads ``cd m``, place the cursor over the ``m`` character and
.. [#] Or another binding that triggers the ``history-pager`` input function. See :doc:`bind <cmds/bind>` for a list.
.. [#] Or another binding that triggers the ``pager-toggle-search`` input function.
.. _private-mode:
Private mode
-------------
Fish has a private mode, in which command history will not be written to the history file on disk. To enable it, either set ``$fish_private_mode`` to a non-empty value, or launch with ``fish --private`` (or ``fish -P`` for short).
If you launch fish with ``-P``, it both hides old history and prevents writing history to disk. This is useful to avoid leaking personal information (e.g. for screencasts) or when dealing with sensitive information.
You can query the variable ``fish_private_mode`` (``if test -n "$fish_private_mode" ...``) if you would like to respect the user's wish for privacy and alter the behavior of your own fish scripts.
Navigating directories
----------------------

View File

@@ -91,7 +91,7 @@ searches for lines ending in ``enabled)`` in ``foo.txt`` (the ``$`` is special t
::
apt install "postgres-*"
apt install "postgres-*"
installs all packages with a name starting with "postgres-", instead of looking through the current directory for files named "postgres-something".
@@ -238,6 +238,37 @@ As a convenience, the pipe ``&|`` redirects both stdout and stderr to the same p
.. [#] A "pager" here is a program that takes output and "paginates" it. ``less`` doesn't just do pages, it allows arbitrary scrolling (even back!).
Combining pipes and redirections
--------------------------------
It is possible to use multiple redirections and a pipe at the same time. In that case, they are read in this order:
1. First the pipe is set up.
2. Then the redirections are evaluated from left-to-right.
This is important when any redirections reference other file descriptors with the ``&N`` syntax. When you say ``>&2``, that will redirect stdout to where stderr is pointing to *at that time*.
Consider this helper function::
# Just make a function that prints something to stdout and stderr
function print
echo out
echo err >&2
end
Now let's see a few cases::
# Redirect both stderr and stdout to less
# (can also be spelt as `&|`)
print 2>&1 | less
# Show the "out" on stderr, silence the "err"
print >&2 2>/dev/null
# Silence both
print >/dev/null 2>&1
.. _syntax-job-control:
Job control
@@ -398,12 +429,14 @@ This uses the :doc:`test <cmds/test>` command to see if the file /etc/os-release
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.::
A more complicated example with a :ref:`command substitution <expand-command-substitution>`::
if test 5 -gt 2
echo "Yes, 5 is greater than 2"
if test "$(uname)" = Linux
echo I like penguins
end
Because ``test`` can be used for many different tests, it is important to quote variables and command substitutions. If the ``$(uname)`` was not quoted, and ``uname`` printed nothing it would run ``test = Linux``, which is an error.
``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
@@ -429,6 +462,13 @@ The :doc:`not <cmds/not>` keyword can be used to invert the status::
echo "You have fish!"
end
Other things commonly used in if-conditions:
- :doc:`contains <cmds/contains>` - to see if a list contains a specific element (``if contains -- /usr/bin $PATH``)
- :doc:`string <cmds/string>` - to e.g. match strings (``if string match -q -- '*-' $arg``)
- :doc:`path <cmds/path>` - to check if paths of some criteria exist (``if path is -rf -- ~/.config/fish/config.fish``)
- :doc:`type <cmds/type>` - to see if a command, function or builtin exists (``if type -q git``)
The ``switch`` statement
^^^^^^^^^^^^^^^^^^^^^^^^
@@ -503,7 +543,7 @@ Loops and blocks
Like most programming language, fish also has the familiar :doc:`while <cmds/while>` and :doc:`for <cmds/for>` loops.
``while`` works like a repeated :doc:`if <cmds/if>`::
``while`` works like a repeated :ref:`if <syntax-if>`::
while true
echo Still running
@@ -652,7 +692,7 @@ Unlike all the other expansions, variable expansion also happens in double quote
Outside of double quotes, variables will expand to as many arguments as they have elements. That means an empty list will expand to nothing, a variable with one element will expand to that element, and a variable with multiple elements will expand to each of those elements separately.
If a variable expands to nothing, it will cancel out any other strings attached to it. See the :ref:`cartesian product <cartesian-product>` section for more information.
If a variable expands to nothing, it will cancel out any other strings attached to it. See the :ref:`Combining Lists <cartesian-product>` section for more information.
Unlike other shells, fish doesn't do what is known as "Word Splitting". Once a variable is set to a particular set of elements, those elements expand as themselves. They aren't split on spaces or newlines or anything::
@@ -693,10 +733,51 @@ 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
Variables as command
''''''''''''''''''''
Like other shells, you can run the value of a variable as a command.
::
> set -g EDITOR emacs
> $EDITOR foo # opens emacs, possibly the GUI version
If you want to give the command an argument inside the variable it needs to be a separate element::
> set EDITOR emacs -nw
> $EDITOR foo # opens emacs in the terminal even if the GUI is installed
> set EDITOR "emacs -nw"
> $EDITOR foo # tries to find a command called "emacs -nw"
Also like other shells, this only works with commands, builtins and functions - it will not work with keywords because they have syntactical importance.
For instance ``set if $if`` won't allow you to make an if-block, and ``set cmd command`` won't allow you to use the :doc:`command <cmds/command>` decorator, but only uses like ``$cmd -q foo``.
.. _expand-command-substitution:
Command substitution
@@ -781,7 +862,7 @@ If there is no "," or variable expansion between the curly braces, they will not
> echo {{a,b}}
{a} {b} # because the inner brace pair is expanded, but the outer isn't.
If after expansion there is nothing between the braces, the argument will be removed (see :ref:`the cartesian product section <cartesian-product>`)::
If after expansion there is nothing between the braces, the argument will be removed (see :ref:`the Combining Lists <cartesian-product>` section)::
> echo foo-{$undefinedvar}
# Output is an empty line, just like a bare `echo`.
@@ -795,49 +876,44 @@ To use a "," as an element, :ref:`quote <quotes>` or :ref:`escape <escapes>` it.
.. _cartesian-product:
Combining lists (Cartesian Product)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Combining lists
^^^^^^^^^^^^^^^
When lists are expanded with other parts attached, they are expanded with these parts still attached. Even if two lists are attached to each other, they are expanded in all combinations. This is referred to as the "cartesian product" (like in mathematics), and works basically like :ref:`brace expansion <expand-brace>`.
When lists are expanded with other parts attached, they are expanded with these parts still attached. That means any string before a list will be concatenated to each element, and two lists will be expanded in all combinations - every element of the first with every element of the second.
This works basically like :ref:`brace expansion <expand-brace>`.
Examples::
# Brace expansion is the most familiar:
# All elements in the brace combine with the parts outside of the braces
# All elements in the brace combine with
# the parts outside of the braces
>_ echo {good,bad}" apples"
good apples bad apples
# The same thing happens with variable expansion.
>_ set -l a x y z
>_ set -l b 1 2 3
# $a is {x,y,z}, $b is {1,2,3},
# so this is `echo {x,y,z}{1,2,3}`
>_ echo $a$b
>_ set -l a x y z; set -l b 1 2 3
>_ echo $a$b # same as {x,y,z}{1,2,3}
x1 y1 z1 x2 y2 z2 x3 y3 z3
# Same thing if something is between the lists
>_ echo $a"-"$b
x-1 y-1 z-1 x-2 y-2 z-2 x-3 y-3 z-3
A result of this is that, if a list has no elements, this combines the string with no elements, which means the entire token is removed!
# Or a brace expansion and a variable
>_ echo {x,y,z}$b
x1 y1 z1 x2 y2 z2 x3 y3 z3
::
# A combined brace-variable expansion
>_ echo {$b}word
1word 2word 3word
# Special case: If $c has no elements, this expands to nothing
>_ set -l c # <- this list is empty!
>_ echo {$c}word
# Output is an empty line
# Output is an empty line - the "word" part is gone
This can be quite useful. For example, if you want to go through all the files in all the directories in :envvar:`PATH`, use
::
for file in $PATH/*
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.
Sometimes this may be unwanted, especially that tokens can disappear after expansion. In those cases, you should double-quote variables - ``echo "$c"word``.
This also happens after :ref:`command substitution <expand-command-substitution>`. To avoid tokens disappearing there, make the inner command return a trailing newline, or store the output in a variable and double-quote it.
E.g.
::
This also happens after :ref:`command substitution <expand-command-substitution>`. To avoid tokens disappearing there, make the inner command return a trailing newline, or double-quote it::
>_ set b 1 2 3
>_ echo (echo x)$b
@@ -850,13 +926,8 @@ E.g.
# so the command substitution expands to an empty string,
# so this is `''banana`
banana
This can be quite useful. For example, if you want to go through all the files in all the directories in :envvar:`PATH`, use
::
for file in $PATH/*
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.
>_ echo "$(printf '%s' '')"banana
# quotes mean this is one argument, the banana stays
.. _expand-slices:
@@ -1009,7 +1080,37 @@ So you set a variable with ``set``, and use it with a ``$`` and the name.
Variable Scope
^^^^^^^^^^^^^^
There are four kinds of variables in fish: universal, global, function and local variables.
All variables in fish have a scope. For example they can be global or local to a function or block::
# This variable is global, we can use it everywhere.
set --global name Patrick
# This variable is local, it will not be visible in a function we call from here.
set --local place "at the Krusty Krab"
function local
# This can find $name, but not $place
echo Hello this is $name $place
# This variable is local, it will not be available
# outside of this function
set --local instrument mayonnaise
echo My favorite instrument is $instrument
# This creates a local $name, and won't touch the global one
set --local name Spongebob
echo My best friend is $name
end
local
# Will print:
# Hello this is Patrick
# My favorite instrument is mayonnaise
# My best friend is Spongebob
echo $name, I am $place and my instrument is $instrument
# Will print:
# Patrick, I am at the Krusty Krab and my instrument is
There are four kinds of variable scopes 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``.
@@ -1080,12 +1181,13 @@ Here is an example of local vs function-scoped variables::
set gnu "In the beginning there was nothing, which exploded"
end
echo $pirate
# 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.
echo $captain
# This will output the good Captain's speech since $captain had function-scope.
# This will output Sir Terry's wisdom.
echo $gnu
# Will output Sir Terry's wisdom.
end
When a function calls another, local variables aren't visible::
@@ -1122,7 +1224,8 @@ 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>`::
@@ -1299,10 +1402,14 @@ 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
@@ -1741,7 +1848,8 @@ 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
@@ -1882,7 +1990,7 @@ To specify a signal handler for the WINCH signal, write::
echo Got WINCH signal!
end
Fish already the following named events for the ``--on-event`` switch:
Fish already has the following named events for the ``--on-event`` switch:
- ``fish_prompt`` is emitted whenever a new fish prompt is about to be displayed.
@@ -1927,4 +2035,24 @@ To start a debug session simply insert the :doc:`builtin command <cmds/breakpoin
Another way to debug script issues is to set the :envvar:`fish_trace` variable, e.g. ``fish_trace=1 fish_prompt`` to see which commands fish executes when running the :doc:`fish_prompt <cmds/fish_prompt>` function.
If you specifically want to debug performance issues, :program:`fish` can be run with the ``--profile /path/to/profile.log`` option to save a profile to the specified path. This profile log includes a breakdown of how long each step in the execution took. See :doc:`fish <cmds/fish>` for more information.
Profiling fish scripts
^^^^^^^^^^^^^^^^^^^^^^
If you specifically want to debug performance issues, :program:`fish` can be run with the ``--profile /path/to/profile.log`` option to save a profile to the specified path. This profile log includes a breakdown of how long each step in the execution took.
For example::
> fish --profile /tmp/sleep.prof -ic 'sleep 3s'
> cat /tmp/sleep.prof
Time Sum Command
3003419 3003419 > sleep 3s
This will show the time for each command itself in the first column, the time for the command and every subcommand (like any commands inside of a :ref:`function <syntax-function>` or :ref:`command substitutions <expand-command-substitution>`) in the second and the command itself in the third, separated with tabs.
The time is given in microseconds.
To see the slowest commands last, ``sort -nk2 /path/to/logfile`` is useful.
For profiling fish's startup there is also ``--profile-startup /path/to/logfile``.
See :doc:`fish <cmds/fish>` for more information.

View File

@@ -175,3 +175,112 @@ products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
----
**License for CMake**
The ``fish`` source code contains files from [CMake](https://cmake.org) to support the build system.
This code is distributed under the terms of a BSD-style license. Copyright 2000-2017 Kitware, Inc.
and Contributors.
The BSD license for CMake follows.
CMake - Cross Platform Makefile Generator
Copyright 2000-2017 Kitware, Inc. and Contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Kitware, Inc. nor the names of Contributors
may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----
**License for code derived from tmux**
``fish`` contains code from [tmux](http://tmux.sourceforge.net), copyrighted by Nicholas Marriott <nicm@users.sourceforge.net> (2007), and made available under the OpenBSD license.
The OpenBSD license is included below.
Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
----
**License for UTF8**
``fish`` also contains small amounts of code under the ISC license, namely the UTF-8 conversion functions. This code is copyright © 2007 Alexey Vatchenko \<av@bsdua.org>.
The ISC license agreement follows.
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
----
**License for flock**
``fish`` also contains small amounts of code from NetBSD, namely the ``flock`` fallback function. This code is copyright 2001 The NetBSD Foundation, Inc., and derived from software contributed to The NetBSD Foundation by Todd Vierling.
The NetBSD license follows.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
----
**MIT License**
``fish`` includes a copy of AngularJS, which is copyright 2010-2012 Google, Inc. and licensed under the MIT License. It also includes the Dracula theme, which is copyright 2018 Dracula Team, and is licensed under the same license.
The MIT license follows.
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.

174
doc_src/prompt.rst Normal file
View File

@@ -0,0 +1,174 @@
Writing your own prompt
=======================
.. only:: builder_man
.. warning::
This document uses formatting to show what a prompt would look like. If you are viewing this in the man page,
you probably want to switch to looking at the html version instead. Run ``help custom-prompt`` to view it in a web browser.
Fish ships a number of prompts that you can view with the :doc:`fish_config <cmds/fish_config>` command, and many users have shared their prompts online.
However, you can also write your own, or adjust an existing prompt. This is a good way to get used to fish's :doc:`scripting language <language>`.
Unlike other shells, fish's prompt is built by running a function - :doc:`fish_prompt <cmds/fish_prompt>`. Or, more specifically, three functions:
- :doc:`fish_prompt <cmds/fish_prompt>`, which is the main prompt function
- :doc:`fish_right_prompt <cmds/fish_right_prompt>`, which is shown on the right side of the terminal.
- :doc:`fish_mode_prompt <cmds/fish_mode_prompt>`, which is shown if :ref:`vi-mode <vi-mode>` is used.
These functions are run, and whatever they print is displayed as the prompt (minus one trailing newline).
Here, we will just be writing a simple fish_prompt.
Our first prompt
----------------
Let's look at a very simple example::
function fish_prompt
echo $PWD '>'
end
This prints the current working directory (:envvar:`PWD`) and a ``>`` symbol to show where the prompt ends. The ``>`` is :ref:`quoted <quotes>` because otherwise it would signify a :ref:`redirection <redirects>`.
Because we've used :doc:`echo <cmds/echo>`, it adds spaces between the two so it ends up looking like (assuming ``_`` is your cursor):
.. role:: white
.. parsed-literal::
:class: highlight
:white:`/home/tutorial >`\ _
Formatting
----------
``echo`` adds spaces between its arguments. If you don't want those, you can use :doc:`string join <cmds/string-join>` like this::
function fish_prompt
string join '' -- $PWD '>'
end
The ``--`` indicates to ``string`` that no options can come after it, in case we extend this with something that can start with a ``-``.
There are other ways to remove the space, including ``echo -s`` and :doc:`printf <cmds/printf>`.
Adding color
------------
This prompt is functional, but a bit boring. We could add some color.
Fortunately, fish offers the :doc:`set_color <cmds/set_color>` command, so you can do::
echo (set_color red)foo
``set_color`` can also handle RGB colors like ``set_color 23b455``, and other formatting options including bold and italics.
So, taking our previous prompt and adding some color::
function fish_prompt
string join '' -- (set_color green) $PWD (set_color normal) '>'
end
A "normal" color tells the terminal to go back to its normal formatting options.
``set_color`` works by producing an escape sequence, which is a special piece of text that terminals
interpret as instructions - for example, to change color. So ``set_color red`` produces the same
effect as::
echo \e\[31mfoo
Although you can write your own escape sequences by hand, it's much easier to use ``set_color``.
Shortening the working directory
--------------------------------
This is fine, but our :envvar:`PWD` can be a bit long, and we are typically only interested in the last few directories. We can shorten this with the :doc:`prompt_pwd <cmds/prompt_pwd>` helper that will give us a shortened working directory::
function fish_prompt
string join '' -- (set_color green) (prompt_pwd) (set_color normal) '>'
end
``prompt_pwd`` takes options to control how much to shorten. For instance, if we want to display the last two directories, we'd use ``prompt_pwd --full-length-dirs 2``::
function fish_prompt
string join '' -- (set_color green) (prompt_pwd --full-length-dirs 2) (set_color normal) '>'
end
With a current directory of "/home/tutorial/Music/Lena Raine/Oneknowing", this would print
.. role:: green
.. parsed-literal::
:class: highlight
:green:`~/M/Lena Raine/Oneknowing`>_
Status
------
One important bit of information that every command returns is the :ref:`status <variables-status>`. This is a whole number from 0 to 255, and usually it is used as an error code - 0 if the command returned successfully, or a number from 1 to 255 if not.
It's useful to display this in your prompt, but showing it when it's 0 seems kind of wasteful.
First of all, since every command (except for :doc:`set <cmds/set>`) changes the status, you need to store it for later use as the first thing in your prompt. Use a :ref:`local variable <variables-scope>` so it will be confined to your prompt function::
set -l last_status $status
And after that, you can set a string if it not zero::
# Prompt status only if it's not 0
set -l stat
if test $last_status -ne 0
set stat (set_color red)"[$last_status]"(set_color normal)
end
And to print it, we add it to our ``string join``::
string join '' -- (set_color green) (prompt_pwd) (set_color normal) $stat '>'
If ``$last_status`` was 0, ``$stat`` is empty, and so it will simply disappear.
So our entire prompt is now::
function fish_prompt
set -l last_status $status
# Prompt status only if it's not 0
set -l stat
if test $last_status -ne 0
set stat (set_color red)"[$last_status]"(set_color normal)
end
string join '' -- (set_color green) (prompt_pwd) (set_color normal) $stat '>'
end
And it looks like:
.. role:: green
.. role:: red
.. parsed-literal::
:class: highlight
:green:`~/M/L/Oneknowing`\ :red:`[1]`>_
after we run ``false`` (which returns 1).
Where to go from here?
----------------------
We have now built a simple but working and usable prompt, but of course more can be done.
- Fish offers more helper functions:
- ``prompt_login`` to describe the user/hostname/container or ``prompt_hostname`` to describe just the host
- ``fish_is_root_user`` to help with changing the symbol for root.
- ``fish_vcs_prompt`` to show version control information (or ``fish_git_prompt`` / ``fish_hg_prompt`` / ``fish_svn_prompt`` to limit it to specific systems)
- You can add a right prompt by changing :doc:`fish_right_prompt <cmds/fish_right_prompt>` or a vi-mode prompt by changing :doc:`fish_mode_prompt <cmds/fish_mode_prompt>`.
- Some prompts have interesting or advanced features
- Add the time when the prompt was printed
- Show various integrations like python's venv
- Color the parts differently.
You can look at fish's sample prompts for inspiration. Open up :doc:`fish_config <cmds/fish_config>`, find one you like and pick it. For example::
fish_config prompt show # <- shows all the sample prompts
fish_config prompt choose disco # <- this picks the "disco" prompt for this session
funced fish_prompt # <- opens fish_prompt in your editor, and reloads it once the editor exits

View File

@@ -14,7 +14,7 @@
{%- macro searchbox() %}
{# modified from sphinx/themes/basic/searchbox.html #}
{%- if builder != "htmlhelp" %}
<div class="inline-search" style="display: none" role="search">
<div class="inline-search" role="search">
<form class="inline-search" action="{{ pathto('search') }}" method="get">
<input placeholder="{{ _('Quick search') }}" type="text" name="q" />
<input type="submit" value="{{ _('Go') }}" />
@@ -22,7 +22,6 @@
<input type="hidden" name="area" value="default" />
</form>
</div>
<script type="text/javascript">$('.inline-search').show(0);</script>
{%- endif %}
{%- endmacro %}
@@ -119,5 +118,4 @@
}
})();
</script>
<script defer type="text/javascript" src="/docs/shared/script.js"></script>
{% endblock %}

View File

@@ -1,5 +1,18 @@
:root {
color-scheme: light dark; /* both supported */
color-scheme: light dark; /* both supported */
--link-color: #0030B3;
--visited-link-color: #6363bb;
--hover-link-color: #00A5F4;
--text-color: #222;
--main-background: #EEEEFA;
--secondary-background: #ddddea;
--outer-background: linear-gradient(to bottom, #a7cfdf 0%,#23538a 100%);
--code-background: rgba(255,255,255, .2);
--code-border: #ac9;
--sidebar-border-color: #ccc;
--secondary-link-color: #444;
--highlight-background: #FFF;
--td-background: white;
}
html {
@@ -8,7 +21,10 @@ html {
}
body {
background: linear-gradient(to bottom, #a7cfdf 0%,#23538a 100%);
background: var(--outer-background);
}
html, body, input {
/* Pick a font.
sans-serif is the Browser default. This is great because the user could change it.
Unfortunately the defaults are decades old and e.g. on Windows still use Arial in Firefox and Edge,
@@ -29,8 +45,7 @@ body {
body {
/* These stay, assuming some browsers pick different defaults */
font-size: 100%;
background-color: #eeeefa;
color: #000;
background-color: var(--main-background);
margin: 0;
padding: 0;
}
@@ -97,7 +112,7 @@ div.sphinxsidebar ul ul, div.sphinxsidebar ul.want-points {
div.sphinxsidebar ul {
margin: 10px;
padding: 0;
color: #444444;
color: var(--secondary-link-color);
margin: 10px;
list-style: none;
}
@@ -132,7 +147,7 @@ div.sphinxsidebar h3 {
}
div.sphinxsidebar h4 {
color: #444444;
color: var(--secondary-link-color);
font-size: 1.3em;
font-weight: normal;
margin: 5px 0 0 0;
@@ -162,7 +177,7 @@ a:hover, div.footer a {
}
div.related a, div.sphinxsidebar a {
color: #444444;
color: var(--secondary-link-color);
}
div.warning {
@@ -192,10 +207,6 @@ div.footer {
font-size: 75%;
}
th, dl.field-list > dt {
background-color: #ede;
}
table.docutils {
border-collapse: collapse;
}
@@ -212,9 +223,9 @@ th > :first-child, td > :first-child {
/* End of SPHINX IMPORT */
div#fmain {
color: #222;
color: var(--text-color);
padding: 1em 2em;
background-color: #EEEEFA;
background-color: var(--main-background);
border-radius: 14px;
position: relative;
margin: 1em auto 1em;
@@ -227,7 +238,8 @@ div#fmain {
div.related {
margin-bottom: 0;
padding: 0.5em 0;
border-top: 1px solid #ccc;
border-top: 1px solid;
border-color: var(--sidebar-border-color);
margin-top: 0;
}
@@ -243,15 +255,10 @@ div.section {
width: 100%;
}
div.related a:hover,
div.footer a:hover,
div.sphinxsidebar a:hover {
color: #0095C4;
}
div.related:first-child {
border-top: 0;
border-bottom: 1px solid #ccc;
border-bottom: 1px solid;
border-color: var(--sidebar-border-color);
}
.inline-search {
@@ -265,7 +272,8 @@ form.inline-search input[type="submit"] {
}
div.sphinxsidebar {
border-right: 1px solid #ccc;
border-right: 1px solid;
border-color: var(--sidebar-border-color);
border-radius: 0px;
line-height: 1em;
font-size: smaller;
@@ -314,18 +322,25 @@ ul li, div.body li {
line-height: 2em;
}
ul.simple p {
ul.simple p, ol.simple p {
/* See "special features" list on index.html */
margin-bottom: 0;
margin-top: 0;
}
ul.simple > li:not(:first-child) > p {
margin-top: 0;
}
ul li dd {
/* nested lists will show up like this, the left margin is massive by default */
margin-left: 0;
}
form.inline-search input,
div.sphinxsidebar input {
border: 1px solid #999999;
border: 1px solid;
border-color: var(--sidebar-border-color);
font-size: smaller;
border-radius: 3px;
}
@@ -364,7 +379,8 @@ div.body hr {
div.body pre, code {
border-radius: 3px;
border: 1px solid #ac9;
border: 1px solid;
border-color: var(--code-border);
}
div.highlight {
@@ -399,28 +415,31 @@ div.body div.seealso {
border: 1px solid #dddd66;
}
div.body a {
color: #0072aa;
a {
color: var(--link-color);
}
div.body a:visited {
color: #6363bb;
color: var(--visited-link-color);
}
div.related a:hover,
div.footer a:hover,
div.sphinxsidebar a:hover,
div.body a:hover {
color: #00B0E4;
color: var(--hover-link-color);
}
code {
/* Make inline-code better visible */
background-color: rgba(20,20,80, .1);
background-color: var(--code-background);
padding-left: 5px;
padding-right: 5px;
margin-left: 3px;
margin-right: 3px;
}
tt, code, pre, dl > dt span ~ em, #synopsis p, #synopsis code, .command {
tt, code, pre, dl > dt span ~ em, #synopsis p, #synopsis code, .command, button {
/* Pick a monospace font.
ui-monospace is the monospace version of system-ui - the system's monospace font.
Unfortunately it's barely supported anywhere (at time of writing only Safari does!),
@@ -467,7 +486,8 @@ div.body tt.xref, div.body a tt, div.body code.xref, div.body a code {
}
table.docutils {
border: 1px solid #ddd;
border: 1px solid;
border-color: var(--sidebar-border-color);
min-width: 20%;
border-radius: 3px;
margin-top: 1em;
@@ -478,7 +498,8 @@ table.docutils {
}
table.docutils td, table.docutils th {
border: 1px solid #ddd !important;
border: 1px solid;
border-color: var(--sidebar-border-color);
border-radius: 3px;
}
@@ -486,13 +507,13 @@ table p, table li {
text-align: left !important;
}
table.docutils th {
background-color: #eee;
th {
background-color: var(--secondary-background);
padding: 0.3em 0.5em;
}
table.docutils td {
background-color: white;
background-color: var(--td-background);
padding: 0.3em 0.5em;
}
@@ -508,26 +529,14 @@ div.footer {
margin-right: 10px;
}
.refcount {
color: #060;
}
.stableabi {
color: #229;
}
.highlight {
background: #FFF;
background: var(--highlight-background);
}
#synopsis p {
font-size: 12pt;
}
dl {
margin-bottom: 1em;
}
dl.envvar, dl.describe {
font-size: 11pt;
font-weight: normal;
@@ -552,6 +561,10 @@ aside.footnote > p {
line-height: 1.5em;
}
div.documentwrapper {
width: 100%;
}
/* On screens that are less than 700px wide remove anything non-essential
- the sidebar, the gradient background, ... */
@media screen and (max-width: 700px) {
@@ -561,9 +574,6 @@ aside.footnote > p {
height: auto;
position: relative;
}
div.documentwrapper {
float: left;
}
div.bodywrapper {
margin-left: 0;
}
@@ -606,6 +616,7 @@ aside.footnote > p {
.gray { color: #777 }
.purple { color: #551a8b; font-weight: bold; }
.red { color: #FF0000; }
.green { color: #00FF00; }
/* Color based on the Name.Function (.nf) class from pygments.css. */
.command { color: #005fd7 }
@@ -621,11 +632,10 @@ aside.footnote > p {
.prompt { color: #8f7902; }
kbd {
background-color: #f9f9f9;
border: 1px solid #aaa;
background-color: var(--td-background);
border: 1px solid;
border-color: var(--sidebar-border-color);
border-radius: .2em;
box-shadow: 0.1em 0.1em 0.2em rgba(0,0,0,0.1);
color: #000;
padding: 0.1em 0.3em;
}
@@ -637,91 +647,38 @@ div.body .internal.reference:link {
content: "$";
}
.footnote, .footnote-reference {
background-color: #ddddea;
background-color: var(--secondary-background);
font-size: 90%;
}
@media (prefers-color-scheme: dark) {
body {
background: linear-gradient(to top, #1f1f3f 0%,#051f3a 100%);
:root {
--link-color: #5fb0fc;
--text-color: #DDD;
--main-background: #202028;
--secondary-background: #112;
--outer-background: linear-gradient(to top, #1f1f3f 0%,#051f3a 100%);
--code-background: rgba(20, 20, 25, .2);
--code-border: #536;
--sidebar-border-color: #666;
--secondary-link-color: #DDD;
--highlight-background: #000;
--td-background: #111;
}
div#fmain {
color: #DDD;
background-color: #202028;
box-shadow: 0 0 5px 1px #000;
}
div.body pre, code {
border: 1px solid #536;
}
.footnote, .footnote-reference {
background-color: #101020;
}
div.sphinxsidebar {
border-right: 1px solid #666;
}
div.related:first-child {
border-bottom: 1px solid #666;
}
div.related {
border-top: 1px solid #666;
}
div.sphinxsidebar a, div.footer {
color: #DDD;
}
div.sphinxsidebar h3 a, div.related a, div.sphinxsidebar h3, div.footer a {
color: #DDD;
}
.highlight {
background: #000;
}
kbd {
background-color: #111;
border: 1px solid #444;
box-shadow: 0.1em 0.1em 0.2em rgba(100,100,100,0.1);
color: #FFF;
}
table.docutils th {
background-color: #222;
}
table.docutils td {
background-color: #111;
}
input {
background-color: #222;
color: #DDD;
}
dt:target, span.highlighted {
background-color: #404060;
}
table.docutils {
border: 1px solid #222;
}
table.docutils td, table.docutils th {
border: 1px solid #222 !important;
}
div.body a {
color: #2092fa;
}
/* Color based on the Name.Function (.nf) class from pygments.css. */
.command { color: #008fd7 }
/* The table background on fishfish Beta r1 */
th, dl.field-list > dt {
background-color: #121;
}
code {
background-color: rgba(200, 200, 255, .2);
}
}

View File

@@ -1,20 +1,19 @@
.highlight .hll { background-color: #ffffcc }
.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */
.highlight .k { color: #204a87; font-weight: bold } /* Keyword */
.highlight .o { color: #00a6b2; } /* Operator */
.highlight .p { color: #00bfff; } /* Punctuation */
.highlight .c { color: #777; font-style: italic; } /* Comment */
.highlight .ch { color: #8f7902; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #8f7902; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #8f7902; font-style: italic } /* Comment.Preproc */
.highlight .cpf { color: #8f7902; font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: #8f7902; font-style: italic } /* Comment.Single */
.highlight .cs { color: #8f7902; font-style: italic } /* Comment.Special */
.highlight .o { color: #005F66; } /* Operator */
.highlight .p { color: #000f8f; } /* Punctuation */
.highlight .c { color: #575757; font-style: italic; } /* Comment */
.highlight .ch { color: #645502; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #645502; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #645502; font-style: italic } /* Comment.Preproc */
.highlight .cpf { color: #645502; font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: #645502; font-style: italic } /* Comment.Single */
.highlight .cs { color: #645502; font-style: italic } /* Comment.Special */
.highlight .gd { color: #a40000 } /* Generic.Deleted */
.highlight .gr { color: #ef2929 } /* Generic.Error */
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.highlight .gi { color: #00A000 } /* Generic.Inserted */
.highlight .gp { color: #8f7902 } /* Generic.Prompt */
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */
.highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */
@@ -24,15 +23,15 @@
.highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */
.highlight .s { color: #4e9a06 } /* Literal.String */
.highlight .s { color: #2F5B06 } /* Literal.String */
.highlight .na { color: #c4a000 } /* Name.Attribute */
.highlight .nb { color: #204a87 } /* Name.Builtin */
.highlight .no { color: #00bfff } /* Name.Constant */
.highlight .no { color: #000f8f } /* Name.Constant */
.highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */
.highlight .ni { color: #ce5c00 } /* Name.Entity */
.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #005fd7 } /* Name.Function */
.highlight .nl { color: #f57900 } /* Name.Label */
.highlight .nf { color: #004BCC; } /* Name.Function */
.highlight .nl { color: #f57900; } /* Name.Label */
.highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */
.highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */
.highlight .w { color: #f8f8f8; } /* Text.Whitespace */
@@ -46,13 +45,13 @@
.highlight .sc { color: #4e9a06 } /* Literal.String.Char */
.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */
.highlight .sd { color: #8f7902; font-style: italic } /* Literal.String.Doc */
.highlight .s2 { color: #4daf08 } /* Literal.String.Double */
.highlight .se { color: #00a6b2 } /* Literal.String.Escape */
.highlight .s2 { color: #2E6506 } /* Literal.String.Double */
.highlight .se { color: #800400 } /* Literal.String.Escape */
.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */
.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */
.highlight .sx { color: #4e9a06 } /* Literal.String.Other */
.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */
.highlight .s1 { color: #d0d00b } /* Literal.String.Single */
.highlight .s1 { color: #605000 } /* Literal.String.Single */
.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */
.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */
.highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */
@@ -66,12 +65,26 @@
:root {
--contrast: #FFFFFF;
}
.highlight .k { color: #507a97; font-weight: bold } /* Keyword */
.highlight .nf { color: #008fd7 } /* Name.Function */
.highlight .nb { color: #209a87 } /* Name.Builtin */
.highlight .no { color: #00bfff } /* Name.Constant */
.highlight .p { color: #00bfff; } /* Punctuation */
.highlight .k { color: #60A3BE; font-weight: bold } /* Keyword */
.highlight .nf { color: #00a0ff } /* Name.Function */
.highlight .nb { color: #A0AfF7 } /* Name.Builtin - we don't emit this, it's for bash scripts*/
.highlight .s { color: #4eFa06 } /* Literal.String */
.highlight .s1 { color: #a0a00b } /* Literal.String.Single */
.highlight .s2 { color: #9ce781 } /* Literal.String.Double */
.highlight .c { color: #969696; font-style: italic; } /* Comment */
.highlight .ch { color: #b19602; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #b19602; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #b19602; font-style: italic } /* Comment.Preproc */
.highlight .cpf { color: #b19602; font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: #b19602; font-style: italic } /* Comment.Single */
.highlight .cs { color: #b19602; font-style: italic } /* Comment.Special */
.highlight .o { color: #00F6b2; } /* Operator */
.highlight .se { color: #00a6b2 } /* Literal.String.Escape */
}
.highlight .g { color: var(--contrast) } /* Generic */
.highlight .gp { color: var(--contrast) } /* Generic.Prompt */
.highlight .l { color: var(--contrast) } /* Literal */
.highlight .n { color: var(--contrast) } /* Name */
.highlight .x { color: var(--contrast) } /* Other */

View File

@@ -27,9 +27,7 @@ which means you are all set up and can start using fish::
you@hostname ~>
This prompt that you see above is the fish default prompt: it shows your username, hostname, and working directory.
- to change this prompt see :ref:`how to change your prompt <prompt>`
- to switch to fish permanently see :ref:`Default Shell <default-shell>`.
This prompt that you see above is the fish default prompt: it shows your username, hostname, and working directory. You can customize it, see :ref:`how to change your prompt <prompt>`.
From now on, we'll pretend your prompt is just a ``>`` to save space.
@@ -79,7 +77,7 @@ Run ``help`` to open fish's help in a web browser, and ``man`` with the page (li
To open this section, use ``help getting-help``.
Fish works by running commands, which are often also installed on your computer. Usually these commands also provide help in the man system, so you can get help for them there. Try ``man ls`` to get help on your computer's ``ls`` command.
This only works for fish's own documentation for itself and its built-in commands (the "builtins"). For any other commands on your system, they should provide their own documentation, often in the man system. For example ``man ls`` should tell you about your computer's ``ls`` command.
Syntax Highlighting
-------------------
@@ -125,55 +123,6 @@ This picks the "none" theme. To see all themes::
Just running ``fish_config`` will open up a browser interface that allows you to pick from the available themes.
Wildcards
---------
Fish supports the familiar wildcard ``*``. To list all JPEG files::
> ls *.jpg
lena.jpg
meena.jpg
santa maria.jpg
You can include multiple wildcards::
> ls l*.p*
lena.png
lesson.pdf
The recursive wildcard ``**`` searches directories recursively::
> ls /var/**.log
/var/log/system.log
/var/run/sntp.log
If that directory traversal is taking a long time, you can :kbd:`Control`\ +\ :kbd:`C` out of it.
For more, see :ref:`Wildcards <expand-wildcard>`.
Pipes and Redirections
----------------------
You can pipe between commands with the usual vertical bar::
> echo hello world | wc
1 2 12
stdin and stdout can be redirected via the familiar ``<`` and ``>``. stderr is redirected with a ``2>``.
::
> grep fish < /etc/shells > ~/output.txt 2> ~/errors.txt
To redirect stdout and stderr into one file, you can use ``&>``::
> make &> make_output.txt
For more, see :ref:`Input and output redirections <redirects>` and :ref:`Pipes <pipes>`.
Autosuggestions
---------------
@@ -366,20 +315,70 @@ You can iterate over a list (or a slice) with a for loop::
# entry: /sbin
# entry: /usr/local/bin
Lists adjacent to other lists or strings are expanded as :ref:`cartesian products <cartesian-product>` unless quoted (see :ref:`Variable expansion <expand-variable>`)::
One particular bit is that you can use lists like :ref:`Brace expansion <expand-brace>`. If you attach another string to a list, it'll combine every element of the list with the string::
> set a 1 2 3
> set 1 a b c
> echo $a$1
1a 2a 3a 1b 2b 3b 1c 2c 3c
> echo $a" banana"
1 banana 2 banana 3 banana
> echo "$a banana"
1 2 3 banana
> set mydirs /usr/bin /bin
> echo $mydirs/fish # this is just like {/usr/bin,/bin}/fish
/usr/bin/fish /bin/fish
This is similar to :ref:`Brace expansion <expand-brace>`.
This also means that, if the list is empty, there will be no argument::
> set empty # no argument
> echo $empty/this_is_gone # prints an empty line
If you quote the list, it will be used as one string and so you'll get one argument even if it is empty.
For more, see :ref:`Lists <variables-lists>`.
For more on combining lists with strings (or even other lists), see :ref:`cartesian products <cartesian-product>` and :ref:`Variable expansion <expand-variable>`.
Wildcards
---------
Fish supports the familiar wildcard ``*``. To list all JPEG files::
> ls *.jpg
lena.jpg
meena.jpg
santa maria.jpg
You can include multiple wildcards::
> ls l*.p*
lena.png
lesson.pdf
The recursive wildcard ``**`` searches directories recursively::
> ls /var/**.log
/var/log/system.log
/var/run/sntp.log
If that directory traversal is taking a long time, you can :kbd:`Control`\ +\ :kbd:`C` out of it.
For more, see :ref:`Wildcards <expand-wildcard>`.
Pipes and Redirections
----------------------
You can pipe between commands with the usual vertical bar::
> echo hello world | wc
1 2 12
stdin and stdout can be redirected via the familiar ``<`` and ``>``. stderr is redirected with a ``2>``.
::
> grep fish < /etc/shells > ~/output.txt 2> ~/errors.txt
To redirect stdout and stderr into one file, you can use ``&>``::
> make &> make_output.txt
For more, see :ref:`Input and output redirections <redirects>` and :ref:`Pipes <pipes>`.
Command Substitutions

57
fish-rust/Cargo.toml Normal file
View File

@@ -0,0 +1,57 @@
[package]
name = "fish-rust"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
[dependencies]
widestring-suffix = { path = "./widestring-suffix/" }
pcre2 = { git = "https://github.com/fish-shell/rust-pcre2", branch = "master", default-features = false, features = ["utf32"] }
fast-float = { git = "https://github.com/fish-shell/fast-float-rust", branch="fish" }
hexponent = { git = "https://github.com/fish-shell/hexponent", branch="fish" }
printf-compat = { git = "https://github.com/fish-shell/printf-compat.git", branch="fish" }
autocxx = "0.23.1"
bitflags = "1.3.2"
cxx = "1.0"
errno = "0.2.8"
inventory = { version = "0.3.3", optional = true}
lazy_static = "1.4.0"
libc = "0.2.137"
lru = "0.10.0"
moveit = "0.5.1"
nix = { version = "0.25.0", default-features = false, features = [] }
num-traits = "0.2.15"
# to make integer->enum conversion easier
num-derive = "0.3.3"
once_cell = "1.17.0"
rand = { version = "0.8.5", features = ["small_rng"] }
unixstring = "0.2.7"
widestring = "1.0.2"
rand_pcg = "0.3.1"
[build-dependencies]
autocxx-build = "0.23.1"
cc = { git = "https://github.com/mqudsi/cc-rs", branch = "fish" }
cxx-build = { git = "https://github.com/fish-shell/cxx", branch = "fish" }
cxx-gen = { git = "https://github.com/fish-shell/cxx", branch = "fish" }
rsconf = { git = "https://github.com/mqudsi/rsconf", branch = "master" }
[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"]
# The following features are auto-detected by the build-script and should not be enabled manually.
asan = []
bsd = []
#cxx = { path = "../../cxx" }
#cxx-gen = { path="../../cxx/gen/lib" }
#autocxx = { path = "../../autocxx" }
#autocxx-build = { path = "../../autocxx/gen/build" }
#autocxx-bindgen = { path = "../../autocxx-bindgen" }

200
fish-rust/build.rs Normal file
View File

@@ -0,0 +1,200 @@
use rsconf::{LinkType, Target};
use std::error::Error;
fn main() {
cc::Build::new().file("src/compat.c").compile("libcompat.a");
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/"));
let mut build = cc::Build::new();
// Add to the default library search path
build.flag_if_supported("-L/usr/local/lib/");
rsconf::add_library_search_path("/usr/local/lib");
let mut detector = Target::new_from(build).unwrap();
// Keep verbose mode on until we've ironed out rust build script stuff
// Note that if autocxx fails to compile any rust code, you'll see the full and unredacted
// stdout/stderr output, which will include things that LOOK LIKE compilation errors as rsconf
// tries to build various test files to try and figure out which libraries and symbols are
// available. IGNORE THESE and scroll to the very bottom of the build script output, past all
// these errors, to see the actual issue.
detector.set_verbose(true);
detect_features(detector);
// 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/ast.rs",
"src/builtins/shared.rs",
"src/builtins/function.rs",
"src/common.rs",
"src/env/env_ffi.rs",
"src/env_dispatch.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/fish_indent.rs",
"src/function.rs",
"src/future_feature_flags.rs",
"src/highlight.rs",
"src/job_group.rs",
"src/kill.rs",
"src/null_terminated_array.rs",
"src/output.rs",
"src/parse_constants.rs",
"src/parse_tree.rs",
"src/parse_util.rs",
"src/print_help.rs",
"src/redirection.rs",
"src/signal.rs",
"src/smoke.rs",
"src/spawn.rs",
"src/termsize.rs",
"src/threads.rs",
"src/timer.rs",
"src/tokenizer.rs",
"src/topic_monitor.rs",
"src/trace.rs",
"src/util.rs",
"src/wait_handle.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().unwrap();
b.flag_if_supported("-std=c++11")
.flag("-Wno-comment")
.compile("fish-rust-autocxx");
rsconf::rebuild_if_paths_changed(&source_files);
}
/// Dynamically enables certain features at build-time, without their having to be explicitly
/// enabled in the `cargo build --features xxx` invocation.
///
/// This can be used to enable features that we check for and conditionally compile according to in
/// our own codebase, but [can't be used to pull in dependencies](0) even if they're gated (in
/// `Cargo.toml`) behind a feature we just enabled.
///
/// [0]: https://github.com/rust-lang/cargo/issues/5499
fn detect_features(target: Target) {
for (feature, handler) in [
// Ignore the first entry, it just sets up the type inference. Model new entries after the
// second line.
(
"",
&(|_: &Target| Ok(false)) as &dyn Fn(&Target) -> Result<bool, Box<dyn Error>>,
),
("bsd", &detect_bsd),
("gettext", &have_gettext),
] {
match handler(&target) {
Err(e) => rsconf::warn!("{}: {}", feature, e),
Ok(true) => rsconf::enable_feature(feature),
Ok(false) => (),
}
}
}
/// Detect if we're being compiled for a BSD-derived OS, allowing targeting code conditionally with
/// `#[cfg(feature = "bsd")]`.
///
/// Rust offers fine-grained conditional compilation per-os for the popular operating systems, but
/// doesn't necessarily include less-popular forks nor does it group them into families more
/// specific than "windows" vs "unix" so we can conditionally compile code for BSD systems.
fn detect_bsd(_: &Target) -> Result<bool, Box<dyn Error>> {
// Instead of using `uname`, we can inspect the TARGET env variable set by Cargo. This lets us
// support cross-compilation scenarios.
let mut target = std::env::var("TARGET").unwrap();
if !target.chars().all(|c| c.is_ascii_lowercase()) {
target = target.to_ascii_lowercase();
}
let result = target.ends_with("bsd") || target.ends_with("dragonfly");
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
assert!(result, "Target incorrectly detected as not BSD!");
Ok(result)
}
/// Detect libintl/gettext and its needed symbols to enable internationalization/localization
/// support.
fn have_gettext(target: &Target) -> Result<bool, Box<dyn Error>> {
// The following script correctly detects and links against gettext, but so long as we are using
// C++ and generate a static library linked into the C++ binary via CMake, we need to account
// for the CMake option WITH_GETTEXT being explicitly disabled.
rsconf::rebuild_if_env_changed("CMAKE_WITH_GETTEXT");
if let Some(with_gettext) = std::env::var_os("CMAKE_WITH_GETTEXT") {
if with_gettext.eq_ignore_ascii_case("0") {
return Ok(false);
}
}
// In order for fish to correctly operate, we need some way of notifying libintl to invalidate
// its localizations when the locale environment variables are modified. Without the libintl
// symbol _nl_msg_cat_cntr, we cannot use gettext even if we find it.
let mut libraries = Vec::new();
let mut found = 0;
let symbols = ["gettext", "_nl_msg_cat_cntr"];
for symbol in &symbols {
// Historically, libintl was required in order to use gettext() and co, but that
// functionality was subsumed by some versions of libc.
if target.has_symbol_in::<&str>(symbol, &[]) {
// No need to link anything special for this symbol
found += 1;
continue;
}
for library in ["intl", "gettextlib"] {
if target.has_symbol(symbol, library) {
libraries.push(library);
found += 1;
continue;
}
}
}
match found {
0 => Ok(false),
1 => Err(format!("gettext found but cannot be used without {}", symbols[1]).into()),
_ => {
rsconf::link_libraries(&libraries, LinkType::Default);
Ok(true)
}
}
}

468
fish-rust/src/abbrs.rs Normal file
View File

@@ -0,0 +1,468 @@
#![allow(clippy::extra_unused_lifetimes, clippy::needless_lifetimes)]
use std::{
collections::HashSet,
sync::{Mutex, MutexGuard},
};
use crate::wchar::prelude::*;
use crate::wchar_ffi::{AsWstr, WCharFromFFI, WCharToFFI};
use cxx::CxxWString;
use once_cell::sync::Lazy;
use crate::abbrs::abbrs_ffi::abbrs_replacer_t;
use crate::parse_constants::SourceRange;
use pcre2::utf32::Regex;
use self::abbrs_ffi::{abbreviation_t, abbrs_position_t, abbrs_replacement_t};
#[cxx::bridge]
mod abbrs_ffi {
extern "C++" {
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<Mutex<AbbreviationSet>> = Lazy::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)
}
pub fn abbrs_get_set() -> MutexGuard<'static, AbbreviationSet> {
abbrs.lock().unwrap()
}
/// 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<Regex>,
/// 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;
}
match &self.regex {
Some(r) => r
.is_match(token.as_char_slice())
.expect("regex match should not error"),
None => 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.as_wstr(), 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.as_wstr(), 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.as_wstr());
}
}
use crate::ffi_tests::add_test;
add_test!("rename_abbrs", || {
use crate::abbrs::{Abbreviation, Position};
use crate::wchar::prelude::*;
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")));
})
});

5731
fish-rust/src/ast.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,595 @@
use super::prelude::*;
use crate::abbrs::{self, Abbreviation, Position};
use crate::common::{escape, escape_string, valid_func_name, EscapeStringStyle};
use crate::env::status::{ENV_NOT_FOUND, ENV_OK};
use crate::env::EnvMode;
use crate::re::{regex_make_anchored, to_boxed_chars};
use pcre2::utf32::{Regex, RegexBuilder};
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 key: &wstr;
let regex: Option<Regex>;
if let Some(regex_pattern) = &opts.regex_pattern {
// Compile the regex as given; if that succeeds then wrap it in our ^$ so it matches the
// entire token.
// We have historically disabled the "(*UTF)" sequence.
let mut builder = RegexBuilder::new();
builder.caseless(false).never_utf(true);
let result = builder.build(to_boxed_chars(regex_pattern));
if let Err(error) = result {
streams.err.append(wgettext_fmt!(
"%ls: Regular expression compile error: %ls\n",
CMD,
error.error_message(),
));
if let Some(offset) = error.offset() {
streams
.err
.append(wgettext_fmt!("%ls: %ls\n", CMD, regex_pattern.as_utfstr()));
streams
.err
.append(wgettext_fmt!("%ls: %*ls\n", CMD, offset, "^"));
}
return STATUS_INVALID_ARGS;
}
let anchored = regex_make_anchored(regex_pattern);
let re = builder
.build(to_boxed_chars(&anchored))
.expect("Anchored compilation should have succeeded");
key = regex_pattern;
regex = Some(re);
} else {
// The name plays double-duty as the token to replace.
key = name;
regex = None;
};
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.to_owned(),
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(arg);
if !esc_src.is_empty() {
let var_name = WString::from_str("_fish_abbr_") + esc_src.as_utfstr();
let ret = parser.remove_var(&var_name, EnvMode::UNIVERSAL.into());
if ret == autocxx::c_int(ENV_OK) {
result = STATUS_CMD_OK
};
}
}
result
})
}
pub fn abbr(
parser: &mut parser_t,
streams: &mut io_streams_t,
argv: &mut [&wstr],
) -> Option<c_int> {
let mut argv_read = Vec::with_capacity(argv.len());
argv_read.extend_from_slice(argv);
let cmd = argv[0];
// Note 1 is returned by wgetopt to indicate a non-option argument.
const NON_OPTION_ARGUMENT: char = 1 as char;
const SET_CURSOR_SHORT: char = 2 as char;
const RENAME_SHORT: char = 3 as char;
// Note the leading '-' causes wgetopter to return arguments in order, instead of permuting
// them. We need this behavior for compatibility with pre-builtin abbreviations where options
// could be given literally, for example `abbr e emacs -nw`.
const short_options: &wstr = L!("-:af:r:seqgUh");
const longopts: &[woption] = &[
wopt(L!("add"), woption_argument_t::no_argument, 'a'),
wopt(L!("position"), woption_argument_t::required_argument, 'p'),
wopt(L!("regex"), woption_argument_t::required_argument, 'r'),
wopt(
L!("set-cursor"),
woption_argument_t::optional_argument,
SET_CURSOR_SHORT,
),
wopt(L!("function"), woption_argument_t::required_argument, 'f'),
wopt(L!("rename"), woption_argument_t::no_argument, RENAME_SHORT),
wopt(L!("erase"), woption_argument_t::no_argument, 'e'),
wopt(L!("query"), woption_argument_t::no_argument, 'q'),
wopt(L!("show"), woption_argument_t::no_argument, 's'),
wopt(L!("list"), woption_argument_t::no_argument, 'l'),
wopt(L!("global"), woption_argument_t::no_argument, 'g'),
wopt(L!("universal"), woption_argument_t::no_argument, 'U'),
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
];
let mut opts = Options::default();
let mut w = wgetopter_t::new(short_options, longopts, argv);
while let Some(c) = w.wgetopt_long() {
match c {
NON_OPTION_ARGUMENT => {
// If --add is specified (or implied by specifying no other commands), all
// unrecognized options after the *second* non-option argument are considered part
// of the abbreviation expansion itself, rather than options to the abbr command.
// For example, `abbr e emacs -nw` works, because `-nw` occurs after the second
// non-option, and --add is implied.
if let Some(arg) = w.woptarg {
opts.args.push(arg.to_owned())
};
if opts.args.len() >= 2
&& !(opts.rename || opts.show || opts.list || opts.erase || opts.query)
{
break;
}
}
'a' => opts.add = true,
'p' => {
if opts.position.is_some() {
streams.err.append(wgettext_fmt!(
"%ls: Cannot specify multiple positions\n",
CMD
));
return STATUS_INVALID_ARGS;
}
if w.woptarg == Some(L!("command")) {
opts.position = Some(Position::Command);
} else if w.woptarg == Some(L!("anywhere")) {
opts.position = Some(Position::Anywhere);
} else {
streams.err.append(wgettext_fmt!(
"%ls: Invalid position '%ls'\n",
CMD,
w.woptarg.unwrap_or_default()
));
streams
.err
.append(L!("Position must be one of: command, anywhere.\n"));
return STATUS_INVALID_ARGS;
}
}
'r' => {
if opts.regex_pattern.is_some() {
streams.err.append(wgettext_fmt!(
"%ls: Cannot specify multiple regex patterns\n",
CMD
));
return STATUS_INVALID_ARGS;
}
opts.regex_pattern = w.woptarg.map(ToOwned::to_owned);
}
SET_CURSOR_SHORT => {
if opts.set_cursor_marker.is_some() {
streams.err.append(wgettext_fmt!(
"%ls: Cannot specify multiple set-cursor options\n",
CMD
));
return STATUS_INVALID_ARGS;
}
// The default set-cursor indicator is '%'.
let _ = opts
.set_cursor_marker
.insert(w.woptarg.unwrap_or(L!("%")).to_owned());
}
'f' => opts.function = w.woptarg.map(ToOwned::to_owned),
RENAME_SHORT => opts.rename = true,
'e' => opts.erase = true,
'q' => opts.query = true,
's' => opts.show = true,
'l' => opts.list = true,
// Kept for backwards compatibility but ignored.
// This basically does nothing now.
'g' => {}
'U' => {
// Kept and made ineffective, so we warn.
streams.err.append(wgettext_fmt!(
"%ls: Warning: Option '%ls' was removed and is now ignored",
cmd,
argv_read[w.woptind - 1]
));
builtin_print_error_trailer(parser, streams, cmd);
}
'h' => {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true);
return STATUS_INVALID_ARGS;
}
'?' => {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], false);
return STATUS_INVALID_ARGS;
}
_ => {
panic!("unexpected retval from wgeopter.next()");
}
}
}
for arg in argv_read[w.woptind..].iter() {
opts.args.push((*arg).into());
}
if !opts.validate(streams) {
return STATUS_INVALID_ARGS;
}
if opts.add {
return abbr_add(&opts, streams);
};
if opts.show {
return abbr_show(streams);
};
if opts.list {
return abbr_list(&opts, streams);
};
if opts.rename {
return abbr_rename(&opts, streams);
};
if opts.erase {
return abbr_erase(&opts, parser);
};
if opts.query {
return abbr_query(&opts);
};
// validate() should error or ensure at least one path is set.
panic!("unreachable");
}

View File

@@ -0,0 +1,997 @@
use std::collections::HashMap;
use super::prelude::*;
use crate::env::{EnvMode, EnvStack};
use crate::wcstringutil::split_string;
use crate::wutil::fish_iswalnum;
const VAR_NAME_PREFIX: &wstr = L!("_flag_");
const BUILTIN_ERR_INVALID_OPT_SPEC: &str = "%ls: Invalid option spec '%ls' at char '%lc'\n";
#[derive(PartialEq)]
enum ArgCardinality {
Optional = -1isize,
None = 0,
Once = 1,
AtLeastOnce = 2,
}
impl Default for ArgCardinality {
fn default() -> Self {
Self::None
}
}
#[derive(Default)]
struct OptionSpec<'args> {
short_flag: char,
long_flag: &'args wstr,
validation_command: &'args wstr,
vals: Vec<WString>,
short_flag_valid: bool,
num_allowed: ArgCardinality,
num_seen: isize,
}
impl OptionSpec<'_> {
fn new(s: char) -> Self {
Self {
short_flag: s,
short_flag_valid: true,
..Default::default()
}
}
}
#[derive(Default)]
struct ArgParseCmdOpts<'args> {
ignore_unknown: bool,
print_help: bool,
stop_nonopt: bool,
min_args: usize,
max_args: usize,
implicit_int_flag: char,
name: WString,
raw_exclusive_flags: Vec<&'args wstr>,
args: Vec<&'args wstr>,
options: HashMap<char, OptionSpec<'args>>,
long_to_short_flag: HashMap<WString, char>,
exclusive_flag_sets: Vec<Vec<char>>,
}
impl ArgParseCmdOpts<'_> {
fn new() -> Self {
Self {
max_args: usize::MAX,
..Default::default()
}
}
}
const SHORT_OPTIONS: &wstr = L!("+:hn:six:N:X:");
const LONG_OPTIONS: &[woption] = &[
wopt(L!("stop-nonopt"), woption_argument_t::no_argument, 's'),
wopt(L!("ignore-unknown"), woption_argument_t::no_argument, 'i'),
wopt(L!("name"), woption_argument_t::required_argument, 'n'),
wopt(L!("exclusive"), woption_argument_t::required_argument, 'x'),
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
wopt(L!("min-args"), woption_argument_t::required_argument, 'N'),
wopt(L!("max-args"), woption_argument_t::required_argument, 'X'),
];
fn exec_subshell(
cmd: &wstr,
parser: &mut parser_t,
outputs: &mut Vec<WString>,
apply_exit_status: bool,
) -> Option<c_int> {
use crate::ffi::exec_subshell_ffi;
use crate::wchar_ffi::wcstring_list_ffi_t;
let mut cmd_output: cxx::UniquePtr<wcstring_list_ffi_t> = wcstring_list_ffi_t::create();
let retval = Some(
exec_subshell_ffi(
cmd.to_ffi().as_ref().unwrap(),
parser.pin(),
cmd_output.pin_mut(),
apply_exit_status,
)
.into(),
);
*outputs = cmd_output.as_mut().unwrap().from_ffi();
retval
}
// Check if any pair of mutually exclusive options was seen. Note that since every option must have
// a short name we only need to check those.
fn check_for_mutually_exclusive_flags(
opts: &ArgParseCmdOpts,
streams: &mut io_streams_t,
) -> Option<c_int> {
for opt_spec in opts.options.values() {
if opt_spec.num_seen == 0 {
continue;
}
// We saw this option at least once. Check all the sets of mutually exclusive options to see
// if this option appears in any of them.
for xarg_set in &opts.exclusive_flag_sets {
if xarg_set.contains(&opt_spec.short_flag) {
// Okay, this option is in a mutually exclusive set of options. Check if any of the
// other mutually exclusive options have been seen.
for xflag in xarg_set {
let Some(xopt_spec) = opts.options.get(xflag) else {
continue;
};
// Ignore this flag in the list of mutually exclusive flags.
if xopt_spec.short_flag == opt_spec.short_flag {
continue;
}
// If it is a different flag check if it has been seen.
if xopt_spec.num_seen != 0 {
let mut flag1: WString = WString::new();
if opt_spec.short_flag_valid {
flag1.push(opt_spec.short_flag);
}
if !opt_spec.long_flag.is_empty() {
if opt_spec.short_flag_valid {
flag1.push('/');
}
flag1.push_utfstr(&opt_spec.long_flag);
}
let mut flag2: WString = WString::new();
if xopt_spec.short_flag_valid {
flag2.push(xopt_spec.short_flag);
}
if !xopt_spec.long_flag.is_empty() {
if xopt_spec.short_flag_valid {
flag2.push('/');
}
flag2.push_utfstr(&xopt_spec.long_flag);
}
// We want the flag order to be deterministic. Primarily to make unit
// testing easier.
if flag1 > flag2 {
std::mem::swap(&mut flag1, &mut flag2);
}
streams.err.append(wgettext_fmt!(
"%ls: %ls %ls: options cannot be used together\n",
opts.name,
flag1,
flag2
));
return STATUS_CMD_ERROR;
}
}
}
}
}
return STATUS_CMD_OK;
}
// This should be called after all the option specs have been parsed. At that point we have enough
// information to parse the values associated with any `--exclusive` flags.
fn parse_exclusive_args(opts: &mut ArgParseCmdOpts, streams: &mut io_streams_t) -> Option<c_int> {
for raw_xflags in &opts.raw_exclusive_flags {
let xflags = split_string(raw_xflags, ',');
if xflags.len() < 2 {
streams.err.append(wgettext_fmt!(
"%ls: exclusive flag string '%ls' is not valid\n",
opts.name,
raw_xflags
));
return STATUS_CMD_ERROR;
}
let exclusive_set: &mut Vec<char> = &mut vec![];
for flag in &xflags {
if flag.char_count() == 1 && opts.options.contains_key(&flag.char_at(0)) {
let short = flag.char_at(0);
// It's a short flag.
exclusive_set.push(short);
} else if let Some(short_equiv) = opts.long_to_short_flag.get(flag) {
// It's a long flag we store as its short flag equivalent.
exclusive_set.push(*short_equiv);
} else {
streams.err.append(wgettext_fmt!(
"%ls: exclusive flag '%ls' is not valid\n",
opts.name,
flag
));
return STATUS_CMD_ERROR;
}
}
// Store the set of exclusive flags for use when parsing the supplied set of arguments.
opts.exclusive_flag_sets.push(exclusive_set.to_vec());
}
return STATUS_CMD_OK;
}
fn parse_flag_modifiers<'args>(
opts: &ArgParseCmdOpts<'args>,
opt_spec: &mut OptionSpec<'args>,
option_spec: &wstr,
opt_spec_str: &mut &'args wstr,
streams: &mut io_streams_t,
) -> bool {
let mut s = *opt_spec_str;
if opt_spec.short_flag == opts.implicit_int_flag && !s.is_empty() && s.char_at(0) != '!' {
streams.err.append(wgettext_fmt!(
"%ls: Implicit int short flag '%lc' does not allow modifiers like '%lc'\n",
opts.name,
opt_spec.short_flag,
s.char_at(0)
));
return false;
}
if s.char_at(0) == '=' {
s = s.slice_from(1);
opt_spec.num_allowed = match s.char_at(0) {
'?' => ArgCardinality::Optional,
'+' => ArgCardinality::AtLeastOnce,
_ => ArgCardinality::Once,
};
if opt_spec.num_allowed != ArgCardinality::Once {
s = s.slice_from(1);
}
}
if s.char_at(0) == '!' {
s = s.slice_from(1);
opt_spec.validation_command = s;
// Move cursor to the end so we don't expect a long flag.
s = s.slice_from(s.char_count());
} else if !s.is_empty() {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_INVALID_OPT_SPEC,
opts.name,
option_spec,
s.char_at(0)
));
return false;
}
// Make sure we have some validation for implicit int flags.
if opt_spec.short_flag == opts.implicit_int_flag && opt_spec.validation_command.is_empty() {
opt_spec.validation_command = L!("_validate_int");
}
if opts.options.contains_key(&opt_spec.short_flag) {
streams.err.append(wgettext_fmt!(
"%ls: Short flag '%lc' already defined\n",
opts.name,
opt_spec.short_flag
));
return false;
}
*opt_spec_str = s;
return true;
}
/// Parse the text following the short flag letter.
fn parse_option_spec_sep<'args>(
opts: &mut ArgParseCmdOpts<'args>,
opt_spec: &mut OptionSpec<'args>,
option_spec: &'args wstr,
opt_spec_str: &mut &'args wstr,
counter: &mut u32,
streams: &mut io_streams_t,
) -> bool {
let mut s = *opt_spec_str;
let mut i = 1usize;
// C++ used -1 to check for # here, we instead adjust opt_spec_str to start one earlier
if s.char_at(i - 1) == '#' {
if s.char_at(i) != '-' {
// Long-only!
i -= 1;
opt_spec.short_flag = char::from_u32(*counter).unwrap();
*counter += 1;
}
if opts.implicit_int_flag != '\0' {
streams.err.append(wgettext_fmt!(
"%ls: Implicit int flag '%lc' already defined\n",
opts.name,
opts.implicit_int_flag
));
return false;
}
opts.implicit_int_flag = opt_spec.short_flag;
opt_spec.short_flag_valid = false;
i += 1;
*opt_spec_str = s.slice_from(i);
return true;
}
match s.char_at(i) {
'-' => {
opt_spec.short_flag_valid = false;
i += 1;
if i == s.char_count() {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_INVALID_OPT_SPEC,
opts.name,
option_spec,
s.char_at(i - 1)
));
return false;
}
}
'/' => {
i += 1; // the struct is initialized assuming short_flag_valid should be true
if i == s.char_count() {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_INVALID_OPT_SPEC,
opts.name,
option_spec,
s.char_at(i - 1)
));
return false;
}
}
'#' => {
if opts.implicit_int_flag != '\0' {
streams.err.append(wgettext_fmt!(
"%ls: Implicit int flag '%lc' already defined\n",
opts.name,
opts.implicit_int_flag
));
return false;
}
opts.implicit_int_flag = opt_spec.short_flag;
opt_spec.num_allowed = ArgCardinality::Once;
i += 1; // the struct is initialized assuming short_flag_valid should be true
}
'!' | '?' | '=' => {
// Try to parse any other flag modifiers
// parse_flag_modifiers assumes opt_spec_str starts where it should, not one earlier
s = s.slice_from(i);
i = 0;
if !parse_flag_modifiers(opts, opt_spec, option_spec, &mut s, streams) {
return false;
}
}
_ => {
// No short flag separator and no other modifiers, so this is a long only option.
// Since getopt needs a wchar, we have a counter that we count up.
opt_spec.short_flag_valid = false;
i -= 1;
opt_spec.short_flag = char::from_u32(*counter).unwrap();
*counter += 1;
}
}
*opt_spec_str = s.slice_from(i);
return true;
}
fn parse_option_spec<'args>(
opts: &mut ArgParseCmdOpts<'args>,
option_spec: &'args wstr,
counter: &mut u32,
streams: &mut io_streams_t,
) -> bool {
if option_spec.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls: An option spec must have at least a short or a long flag\n",
opts.name
));
return false;
}
let mut s = option_spec;
if !fish_iswalnum(s.char_at(0)) && s.char_at(0) != '#' {
streams.err.append(wgettext_fmt!(
"%ls: Short flag '%lc' invalid, must be alphanum or '#'\n",
opts.name,
s.char_at(0)
));
return false;
}
let mut opt_spec = OptionSpec::new(s.char_at(0));
// Try parsing stuff after the short flag.
if s.char_count() > 1
&& !parse_option_spec_sep(opts, &mut opt_spec, option_spec, &mut s, counter, streams)
{
return false;
}
// Collect any long flag name.
if !s.is_empty() {
let long_flag_char_count = s
.chars()
.take_while(|&c| c == '-' || c == '_' || fish_iswalnum(c))
.count();
if long_flag_char_count > 0 {
opt_spec.long_flag = s.slice_to(long_flag_char_count);
if opts.long_to_short_flag.contains_key(opt_spec.long_flag) {
streams.err.append(wgettext_fmt!(
"%ls: Long flag '%ls' already defined\n",
opts.name,
opt_spec.long_flag
));
return false;
}
}
s = s.slice_from(long_flag_char_count);
}
if !parse_flag_modifiers(opts, &mut opt_spec, option_spec, &mut s, streams) {
return false;
}
// Record our long flag if we have one.
if !opt_spec.long_flag.is_empty() {
let ins = opts
.long_to_short_flag
.insert(WString::from(opt_spec.long_flag), opt_spec.short_flag);
assert!(ins.is_none(), "Should have inserted long flag");
}
// Record our option under its short flag.
opts.options.insert(opt_spec.short_flag, opt_spec);
return true;
}
fn collect_option_specs<'args>(
opts: &mut ArgParseCmdOpts<'args>,
optind: &mut usize,
argc: usize,
args: &[&'args wstr],
streams: &mut io_streams_t,
) -> Option<c_int> {
let cmd: &wstr = args[0];
// A counter to give short chars to long-only options because getopt needs that.
// Luckily we have wgetopt so we can use wchars - this is one of the private use areas so we
// have 6400 options available.
let mut counter = 0xE000u32;
loop {
if *optind == argc {
streams
.err
.append(wgettext_fmt!("%ls: Missing -- separator\n", cmd));
return STATUS_INVALID_ARGS;
}
if "--" == args[*optind] {
*optind += 1;
break;
}
if !parse_option_spec(opts, args[*optind], &mut counter, streams) {
return STATUS_CMD_ERROR;
}
*optind += 1;
}
// Check for counter overreach once at the end because this is very unlikely to ever be reached.
let counter_max = 0xF8FFu32;
if counter > counter_max {
streams
.err
.append(wgettext_fmt!("%ls: Too many long-only options\n", cmd));
return STATUS_INVALID_ARGS;
}
return STATUS_CMD_OK;
}
fn parse_cmd_opts<'args>(
opts: &mut ArgParseCmdOpts<'args>,
optind: &mut usize,
argc: usize,
args: &mut [&'args wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Option<c_int> {
let cmd = args[0];
let mut args_read = Vec::with_capacity(args.len());
args_read.extend_from_slice(args);
let mut w = wgetopter_t::new(SHORT_OPTIONS, LONG_OPTIONS, args);
while let Some(c) = w.wgetopt_long() {
match c {
'n' => opts.name = w.woptarg.unwrap().to_owned(),
's' => opts.stop_nonopt = true,
'i' => opts.ignore_unknown = true,
// Just save the raw string here. Later, when we have all the short and long flag
// definitions we'll parse these strings into a more useful data structure.
'x' => opts.raw_exclusive_flags.push(w.woptarg.unwrap()),
'h' => opts.print_help = true,
'N' => {
opts.min_args = {
let x = fish_wcstol(w.woptarg.unwrap()).unwrap_or(-1);
if x < 0 {
streams.err.append(wgettext_fmt!(
"%ls: Invalid --min-args value '%ls'\n",
cmd,
w.woptarg.unwrap()
));
return STATUS_INVALID_ARGS;
}
x.try_into().unwrap()
}
}
'X' => {
opts.max_args = {
let x = fish_wcstol(w.woptarg.unwrap()).unwrap_or(-1);
if x < 0 {
streams.err.append(wgettext_fmt!(
"%ls: Invalid --max-args value '%ls'\n",
cmd,
w.woptarg.unwrap()
));
return STATUS_INVALID_ARGS;
}
x.try_into().unwrap()
}
}
':' => {
builtin_missing_argument(
parser,
streams,
cmd,
args[w.woptind - 1],
/* print_hints */ false,
);
return STATUS_INVALID_ARGS;
}
'?' => {
builtin_unknown_option(parser, streams, cmd, args[w.woptind - 1], false);
return STATUS_INVALID_ARGS;
}
_ => panic!("unexpected retval from wgetopt_long"),
}
}
if opts.print_help {
return STATUS_CMD_OK;
}
if "--" == args_read[w.woptind - 1] {
w.woptind -= 1;
}
if argc == w.woptind {
// The user didn't specify any option specs.
streams
.err
.append(wgettext_fmt!("%ls: Missing -- separator\n", cmd));
return STATUS_INVALID_ARGS;
}
if opts.name.is_empty() {
// If no name has been given, we default to the function name.
// If any error happens, the backtrace will show which argparse it was.
opts.name = parser
.get_func_name(1)
.unwrap_or_else(|| L!("argparse").to_owned());
}
*optind = w.woptind;
return collect_option_specs(opts, optind, argc, args, streams);
}
fn populate_option_strings<'args>(
opts: &ArgParseCmdOpts<'args>,
short_options: &mut WString,
long_options: &mut Vec<woption<'args>>,
) {
for opt_spec in opts.options.values() {
if opt_spec.short_flag_valid {
short_options.push(opt_spec.short_flag);
}
let arg_type = match opt_spec.num_allowed {
ArgCardinality::Optional => {
if opt_spec.short_flag_valid {
short_options.push_str("::");
}
woption_argument_t::optional_argument
}
ArgCardinality::Once | ArgCardinality::AtLeastOnce => {
if opt_spec.short_flag_valid {
short_options.push_str(":");
}
woption_argument_t::required_argument
}
ArgCardinality::None => woption_argument_t::no_argument,
};
if !opt_spec.long_flag.is_empty() {
long_options.push(wopt(opt_spec.long_flag, arg_type, opt_spec.short_flag));
}
}
}
fn validate_arg<'opts>(
parser: &mut parser_t,
opts_name: &wstr,
opt_spec: &mut OptionSpec<'opts>,
is_long_flag: bool,
woptarg: &'opts wstr,
streams: &mut io_streams_t,
) -> Option<c_int> {
// Obviously if there is no arg validation command we assume the arg is okay.
if opt_spec.validation_command.is_empty() {
return STATUS_CMD_OK;
}
let vars = parser.get_vars();
vars.push(true /* new_scope */);
let env_mode = EnvMode::LOCAL | EnvMode::EXPORT;
vars.set_one(L!("_argparse_cmd"), env_mode, opts_name.to_owned());
let flag_name = WString::from(VAR_NAME_PREFIX) + "name";
if is_long_flag {
vars.set_one(&flag_name, env_mode, opt_spec.long_flag.to_owned());
} else {
vars.set_one(
&flag_name,
env_mode,
WString::from_chars(vec![opt_spec.short_flag]),
);
}
vars.set_one(
&(WString::from(VAR_NAME_PREFIX) + "value"),
env_mode,
woptarg.to_owned(),
);
let mut cmd_output = Vec::new();
let retval = exec_subshell(opt_spec.validation_command, parser, &mut cmd_output, false);
for output in cmd_output {
streams.err.appendln(output);
}
vars.pop();
return retval;
}
/// \return whether the option 'opt' is an implicit integer option.
fn is_implicit_int(opts: &ArgParseCmdOpts, val: &wstr) -> bool {
if opts.implicit_int_flag == '\0' {
// There is no implicit integer option.
return false;
}
// We succeed if this argument can be parsed as an integer.
fish_wcstol(val).is_ok()
}
// Store this value under the implicit int option.
fn validate_and_store_implicit_int<'args>(
parser: &mut parser_t,
opts: &mut ArgParseCmdOpts<'args>,
val: &'args wstr,
w: &mut wgetopter_t,
is_long_flag: bool,
streams: &mut io_streams_t,
) -> Option<c_int> {
let opt_spec = opts.options.get_mut(&opts.implicit_int_flag).unwrap();
let retval = validate_arg(parser, &opts.name, opt_spec, is_long_flag, val, streams);
if retval != STATUS_CMD_OK {
return retval;
}
// It's a valid integer so store it and return success.
opt_spec.vals.clear();
opt_spec.vals.push(val.into());
opt_spec.num_seen += 1;
w.nextchar = L!("");
return STATUS_CMD_OK;
}
fn handle_flag<'args>(
parser: &mut parser_t,
opts: &mut ArgParseCmdOpts<'args>,
opt: char,
is_long_flag: bool,
woptarg: Option<&'args wstr>,
streams: &mut io_streams_t,
) -> Option<c_int> {
let opt_spec = opts.options.get_mut(&opt).unwrap();
opt_spec.num_seen += 1;
if opt_spec.num_allowed == ArgCardinality::None {
// It's a boolean flag. Save the flag we saw since it might be useful to know if the
// short or long flag was given.
assert!(woptarg.is_none());
let s = if is_long_flag {
WString::from("--") + opt_spec.long_flag
} else {
WString::from_chars(['-', opt_spec.short_flag])
};
opt_spec.vals.push(s);
return STATUS_CMD_OK;
}
if let Some(woptarg) = woptarg {
let retval = validate_arg(parser, &opts.name, opt_spec, is_long_flag, woptarg, streams);
if retval != STATUS_CMD_OK {
return retval;
}
}
match opt_spec.num_allowed {
ArgCardinality::Optional | ArgCardinality::Once => {
// We're depending on `wgetopt_long()` to report that a mandatory value is missing if
// `opt_spec->num_allowed == 1` and thus return ':' so that we don't take this branch if
// the mandatory arg is missing.
opt_spec.vals.clear();
if let Some(arg) = woptarg {
opt_spec.vals.push(arg.into());
}
}
_ => {
opt_spec.vals.push(woptarg.unwrap().into());
}
}
return STATUS_CMD_OK;
}
fn argparse_parse_flags<'args>(
parser: &mut parser_t,
opts: &mut ArgParseCmdOpts<'args>,
argc: usize,
args: &mut [&'args wstr],
optind: &mut usize,
streams: &mut io_streams_t,
) -> Option<c_int> {
let mut args_read = Vec::with_capacity(args.len());
args_read.extend_from_slice(args);
// "+" means stop at nonopt, "-" means give nonoptions the option character code `1`, and don't
// reorder.
let mut short_options = WString::from(if opts.stop_nonopt { L!("+:") } else { L!("-:") });
let mut long_options = vec![];
populate_option_strings(opts, &mut short_options, &mut long_options);
let mut long_idx: usize = usize::MAX;
let mut w = wgetopter_t::new(&short_options, &long_options, args);
while let Some(opt) = w.wgetopt_long_idx(&mut long_idx) {
let retval = match opt {
':' => {
builtin_missing_argument(
parser,
streams,
&opts.name,
args_read[w.woptind - 1],
false,
);
STATUS_INVALID_ARGS
}
'?' => {
// It's not a recognized flag. See if it's an implicit int flag.
let arg_contents = &args_read[w.woptind - 1].slice_from(1);
if is_implicit_int(opts, arg_contents) {
validate_and_store_implicit_int(
parser,
opts,
arg_contents,
&mut w,
long_idx != usize::MAX,
streams,
)
} else if !opts.ignore_unknown {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_UNKNOWN,
opts.name,
args_read[w.woptind - 1]
));
STATUS_INVALID_ARGS
} else {
// Any unrecognized option is put back if ignore_unknown is used.
// This allows reusing the same argv in multiple argparse calls,
// or just ignoring the error (e.g. in completions).
opts.args.push(args_read[w.woptind - 1]);
// Work around weirdness with wgetopt, which crashes if we `continue` here.
if w.woptind == argc {
break;
}
// Explain to wgetopt that we want to skip to the next arg,
// because we can't handle this opt group.
w.nextchar = L!("");
STATUS_CMD_OK
}
}
NONOPTION_CHAR_CODE => {
// A non-option argument.
// We use `-` as the first option-string-char to disable GNU getopt's reordering,
// otherwise we'd get ignored options first and normal arguments later.
// E.g. `argparse -i -- -t tango -w` needs to keep `-t tango -w` in $argv, not `-t -w
// tango`.
opts.args.push(args_read[w.woptind - 1]);
continue;
}
// It's a recognized flag.
_ => handle_flag(
parser,
opts,
opt,
long_idx != usize::MAX,
w.woptarg,
streams,
),
};
if retval != STATUS_CMD_OK {
return retval;
}
long_idx = usize::MAX;
}
*optind = w.woptind;
return STATUS_CMD_OK;
}
// This function mimics the `wgetopt_long()` usage found elsewhere in our other builtin commands.
// It's different in that the short and long option structures are constructed dynamically based on
// arguments provided to the `argparse` command.
fn argparse_parse_args<'args>(
opts: &mut ArgParseCmdOpts<'args>,
args: &mut [&'args wstr],
argc: usize,
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Option<c_int> {
if argc <= 1 {
return STATUS_CMD_OK;
}
let mut optind = 0usize;
let retval = argparse_parse_flags(parser, opts, argc, args, &mut optind, streams);
if retval != STATUS_CMD_OK {
return retval;
}
let retval = check_for_mutually_exclusive_flags(opts, streams);
if retval != STATUS_CMD_OK {
return retval;
}
opts.args.extend_from_slice(&args[optind..]);
return STATUS_CMD_OK;
}
fn check_min_max_args_constraints(
opts: &ArgParseCmdOpts,
streams: &mut io_streams_t,
) -> Option<c_int> {
let cmd = &opts.name;
if opts.args.len() < opts.min_args {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_MIN_ARG_COUNT1,
cmd,
opts.min_args,
opts.args.len()
));
return STATUS_CMD_ERROR;
}
if opts.max_args != usize::MAX && opts.args.len() > opts.max_args {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_MAX_ARG_COUNT1,
cmd,
opts.max_args,
opts.args.len()
));
return STATUS_CMD_ERROR;
}
return STATUS_CMD_OK;
}
/// Put the result of parsing the supplied args into the caller environment as local vars.
fn set_argparse_result_vars(vars: &EnvStack, opts: &ArgParseCmdOpts) {
for opt_spec in opts.options.values() {
if opt_spec.num_seen == 0 {
continue;
}
if opt_spec.short_flag_valid {
let mut var_name = WString::from(VAR_NAME_PREFIX);
var_name.push(opt_spec.short_flag);
vars.set(&var_name, EnvMode::LOCAL, opt_spec.vals.clone());
}
if !opt_spec.long_flag.is_empty() {
// We do a simple replacement of all non alphanum chars rather than calling
// escape_string(long_flag, 0, STRING_STYLE_VAR).
let long_flag = opt_spec
.long_flag
.chars()
.map(|c| if fish_iswalnum(c) { c } else { '_' });
let var_name_long: WString = VAR_NAME_PREFIX.chars().chain(long_flag).collect();
vars.set(&var_name_long, EnvMode::LOCAL, opt_spec.vals.clone());
}
}
let args = opts.args.iter().map(|&s| s.to_owned()).collect();
vars.set(L!("argv"), EnvMode::LOCAL, args);
}
/// The argparse builtin. This is explicitly not compatible with the BSD or GNU version of this
/// command. That's because fish doesn't have the weird quoting problems of POSIX shells. So we
/// don't need to support flags like `--unquoted`. Similarly we don't want to support introducing
/// long options with a single dash so we don't support the `--alternative` flag. That `getopt` is
/// an external command also means its output has to be in a form that can be eval'd. Because our
/// version is a builtin it can directly set variables local to the current scope (e.g., a
/// function). It doesn't need to write anything to stdout that then needs to be eval'd.
pub fn argparse(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let cmd = args[0];
let argc = args.len();
let mut opts = ArgParseCmdOpts::new();
let mut optind = 0usize;
let retval = parse_cmd_opts(&mut opts, &mut optind, argc, args, parser, streams);
if retval != STATUS_CMD_OK {
// This is an error in argparse usage, so we append the error trailer with a stack trace.
// The other errors are an error in using *the command* that is using argparse,
// so our help doesn't apply.
builtin_print_error_trailer(parser, streams, cmd);
return retval;
}
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
let retval = parse_exclusive_args(&mut opts, streams);
if retval != STATUS_CMD_OK {
return retval;
}
// wgetopt expects the first argument to be the command, and skips it.
// if optind was 0 we'd already have returned.
assert!(optind > 0, "Optind is 0?");
let retval = argparse_parse_args(
&mut opts,
&mut args[optind - 1..],
argc - optind + 1,
parser,
streams,
);
if retval != STATUS_CMD_OK {
return retval;
}
let retval = check_min_max_args_constraints(&opts, streams);
if retval != STATUS_CMD_OK {
return retval;
}
set_argparse_result_vars(&parser.get_vars(), &opts);
return retval;
}

View File

@@ -0,0 +1,129 @@
// Implementation of the bg builtin.
use std::pin::Pin;
use super::prelude::*;
/// 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()
));
job.get_job_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.
let mut pids: Vec<libc::pid_t> = Vec::new();
// 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: Option<i32> = STATUS_CMD_OK;
for arg in &args[opts.optind..] {
let pid = fish_wcstoi(arg);
#[allow(clippy::unnecessary_unwrap)]
if pid.is_err() || pid.unwrap() < 0 {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is not a valid job specifier\n",
cmd,
arg
));
retval = STATUS_INVALID_ARGS;
} else {
pids.push(pid.unwrap());
}
}
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(autocxx::c_int(pid), Pin::new(&mut job_pos))
.as_ref()
};
if job.is_some() {
send_to_bg(parser, streams, cmd, job_pos);
} else {
streams
.err
.append(wgettext_fmt!("%ls: Could not find job '%d'\n", cmd, pid));
}
}
return STATUS_CMD_OK;
}

View File

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

View File

@@ -0,0 +1,86 @@
use super::prelude::*;
use crate::ffi::{builtin_exists, builtin_get_names_ffi};
#[derive(Default)]
struct builtin_cmd_opts_t {
query: bool,
list_names: bool,
}
pub fn r#builtin(
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;
let mut opts: builtin_cmd_opts_t = Default::default();
const shortopts: &wstr = L!(":hnq");
const longopts: &[woption] = &[
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
wopt(L!("names"), woption_argument_t::no_argument, 'n'),
wopt(L!("query"), woption_argument_t::no_argument, 'q'),
];
let mut w = wgetopter_t::new(shortopts, longopts, argv);
while let Some(c) = w.wgetopt_long() {
match c {
'q' => opts.query = true,
'n' => opts.list_names = true,
'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()");
}
}
}
if opts.query && opts.list_names {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
wgettext!("--query and --names are mutually exclusive")
));
return STATUS_INVALID_ARGS;
}
// If we don't have either, we print our help.
// This is also what e.g. command and time,
// the other decorator/builtins do.
if !opts.query && !opts.list_names {
builtin_print_help(parser, streams, cmd);
return STATUS_INVALID_ARGS;
}
if opts.query {
let optind = w.woptind;
for arg in argv.iter().take(argc).skip(optind) {
if builtin_exists(&arg.to_ffi()) {
return STATUS_CMD_OK;
}
}
return STATUS_CMD_ERROR;
}
if opts.list_names {
// List is guaranteed to be sorted by name.
let names: Vec<WString> = builtin_get_names_ffi().from_ffi();
for name in names {
streams.out.appendln(name);
}
}
STATUS_CMD_OK
}

View File

@@ -0,0 +1,172 @@
// Implementation of the cd builtin.
use super::prelude::*;
use crate::{
env::{EnvMode, Environment},
fds::{wopen_cloexec, AutoCloseFd},
path::path_apply_cdpath,
wutil::{normalize_path, wperror, wreadlink},
};
use errno::{self, Errno};
use libc::{fchdir, EACCES, ELOOP, ENOENT, ENOTDIR, EPERM, O_RDONLY};
// The cd builtin. Changes the current directory to the one specified or to $HOME if none is
// specified. The directory can be relative to any directory in the CDPATH variable.
pub fn cd(parser: &mut parser_t, streams: &mut io_streams_t, args: &mut [&wstr]) -> Option<c_int> {
let cmd = args[0];
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:?}"),
};
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
let vars = parser.get_vars();
let tmpstr;
let dir_in: &wstr = if args.len() > opts.optind {
args[opts.optind]
} else {
match vars.get_unless_empty(L!("HOME")) {
Some(v) => {
tmpstr = v.as_string();
&tmpstr
}
None => {
streams
.err
.append(wgettext_fmt!("%ls: Could not find home directory\n", cmd));
return STATUS_CMD_ERROR;
}
}
};
// Stop `cd ""` from crashing
if dir_in.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls: Empty directory '%ls' does not exist\n",
cmd,
dir_in
));
if !parser.is_interactive() {
streams.err.append(parser.pin().current_line().from_ffi());
};
return STATUS_CMD_ERROR;
}
let pwd = vars.get_pwd_slash();
let dirs = path_apply_cdpath(dir_in, &pwd, vars.as_ref());
if dirs.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls: The directory '%ls' does not exist\n",
cmd,
dir_in
));
if !parser.is_interactive() {
streams.err.append(parser.pin().current_line().from_ffi());
}
return STATUS_CMD_ERROR;
}
let mut best_errno = 0;
let mut broken_symlink = WString::new();
let mut broken_symlink_target = WString::new();
for dir in dirs {
let norm_dir = normalize_path(&dir, true);
errno::set_errno(Errno(0));
// We need to keep around the fd for this directory, in the parser.
let mut dir_fd = AutoCloseFd::new(wopen_cloexec(&norm_dir, O_RDONLY, 0));
if !(dir_fd.is_valid() && unsafe { fchdir(dir_fd.fd()) } == 0) {
// Some errors we skip and only report if nothing worked.
// ENOENT in particular is very low priority
// - if in another directory there was a *file* by the correct name
// we prefer *that* error because it's more specific
if errno::errno().0 == ENOENT {
let tmp = wreadlink(&norm_dir);
// clippy doesn't like this is_some/unwrap pair, but using if let is harder to read IMO
#[allow(clippy::unnecessary_unwrap)]
if broken_symlink.is_empty() && tmp.is_some() {
broken_symlink = norm_dir;
broken_symlink_target = tmp.unwrap();
} else if best_errno == 0 {
best_errno = errno::errno().0;
}
continue;
} else if errno::errno().0 == ENOTDIR {
best_errno = errno::errno().0;
continue;
}
best_errno = errno::errno().0;
break;
}
// Stash the fd for the cwd in the parser.
parser.pin().set_cwd_fd(autocxx::c_int(dir_fd.acquire()));
parser.pin().set_var_and_fire(
&L!("PWD").to_ffi(),
EnvMode::EXPORT.bits() | EnvMode::GLOBAL.bits(),
norm_dir,
);
return STATUS_CMD_OK;
}
if best_errno == ENOTDIR {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is not a directory\n",
cmd,
dir_in
));
} else if !broken_symlink.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is a broken symbolic link to '%ls'\n",
cmd,
broken_symlink,
broken_symlink_target
));
} else if best_errno == ELOOP {
streams.err.append(wgettext_fmt!(
"%ls: Too many levels of symbolic links: '%ls'\n",
cmd,
dir_in
));
} else if best_errno == ENOENT {
streams.err.append(wgettext_fmt!(
"%ls: The directory '%ls' does not exist\n",
cmd,
dir_in
));
} else if best_errno == EACCES || best_errno == EPERM {
streams.err.append(wgettext_fmt!(
"%ls: Permission denied: '%ls'\n",
cmd,
dir_in
));
} else {
errno::set_errno(Errno(best_errno));
wperror(L!("cd"));
streams.err.append(wgettext_fmt!(
"%ls: Unknown error trying to locate directory '%ls'\n",
cmd,
dir_in
));
}
if !parser.is_interactive() {
streams.err.append(parser.pin().current_line().from_ffi());
}
return STATUS_CMD_ERROR;
}

View File

@@ -0,0 +1,92 @@
use super::prelude::*;
use crate::path::{path_get_path, path_get_paths};
#[derive(Default)]
struct command_cmd_opts_t {
all: bool,
quiet: bool,
find_path: bool,
}
pub fn r#command(
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;
let mut opts: command_cmd_opts_t = Default::default();
const shortopts: &wstr = L!(":hasqv");
const longopts: &[woption] = &[
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
wopt(L!("all"), woption_argument_t::no_argument, 'a'),
wopt(L!("query"), woption_argument_t::no_argument, 'q'),
wopt(L!("quiet"), woption_argument_t::no_argument, 'q'),
wopt(L!("search"), woption_argument_t::no_argument, 's'),
];
let mut w = wgetopter_t::new(shortopts, longopts, argv);
while let Some(c) = w.wgetopt_long() {
match c {
'a' => opts.all = true,
'q' => opts.quiet = true,
's' => opts.find_path = true,
// -s and -v are aliases
'v' => opts.find_path = true,
'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()");
}
}
}
// Quiet implies find_path.
if !opts.find_path && !opts.all && !opts.quiet {
builtin_print_help(parser, streams, cmd);
return STATUS_INVALID_ARGS;
}
let mut res = false;
let optind = w.woptind;
for arg in argv.iter().take(argc).skip(optind) {
let paths = if opts.all {
path_get_paths(arg, &*parser.get_vars())
} else {
match path_get_path(arg, &*parser.get_vars()) {
Some(p) => vec![p],
None => vec![],
}
};
for path in paths.iter() {
res = true;
if opts.quiet {
return STATUS_CMD_OK;
}
streams.out.appendln(path);
if !opts.all {
break;
}
}
}
if res {
STATUS_CMD_OK
} else {
STATUS_CMD_UNKNOWN
}
}

View File

@@ -0,0 +1,84 @@
// Implementation of the contains builtin.
use super::prelude::*;
#[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.appendln(i.to_wstring());
}
return STATUS_CMD_OK;
}
}
} else {
streams
.err
.append(wgettext_fmt!("%ls: Key not specified\n", cmd));
}
return STATUS_CMD_ERROR;
}

View File

@@ -0,0 +1,33 @@
use super::prelude::*;
// How many bytes we read() at once.
// Since this is just for counting, it can be massive.
const COUNT_CHUNK_SIZE: usize = 512 * 256;
/// Implementation of the builtin count command, used to count the number of arguments sent to it.
pub fn count(
_parser: &mut parser_t,
streams: &mut io_streams_t,
argv: &mut [&wstr],
) -> Option<c_int> {
// Always add the size of argv (minus 0, which is "count").
// That means if you call `something | count a b c`, you'll get the count of something _plus 3_.
let mut numargs = argv.len() - 1;
// (silly variable for Arguments to do nothing with)
let mut zero = 0;
// Count the newlines coming in via stdin like `wc -l`.
// This means excluding lines that don't end in a newline!
numargs += Arguments::new(&[] as _, &mut zero, streams, COUNT_CHUNK_SIZE)
// second is "want_newline" - whether the line ended in a newline
.filter(|x| x.1)
.count();
streams.out.appendln(numargs.to_wstring());
if numargs == 0 {
return STATUS_CMD_ERROR;
}
STATUS_CMD_OK
}

View File

@@ -0,0 +1,228 @@
//! Implementation of the echo builtin.
use super::prelude::*;
use crate::wchar::encode_byte_to_char;
#[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).
encode_byte_to_char(narrow_val)
} else {
consumed = 0;
'\\'
}
}
};
// Skip over characters that were part of this escape sequence (after the backslash
// that was consumed by the `while` loop).
// TODO: `Iterator::advance_by()`: https://github.com/rust-lang/rust/issues/77404
for _ in 0..consumed {
let _ = chars.next();
}
out.push(escaped);
}
}
if opts.print_newline && !output_stopped {
out.push('\n');
}
if !out.is_empty() {
streams.out.append(out);
}
STATUS_CMD_OK
}

View File

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

View File

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

View File

@@ -0,0 +1,432 @@
use super::prelude::*;
use crate::ast::BlockStatement;
use crate::common::{valid_func_name, valid_var_name};
use crate::env::environment::Environment;
use crate::event::{self, EventDescription, EventHandler};
use crate::ffi::io_streams_t as io_streams_ffi_t;
use crate::function;
use crate::global_safety::RelaxedAtomicBool;
use crate::parse_tree::NodeRef;
use crate::parse_tree::ParsedSourceRefFFI;
use crate::parser_keywords::parser_keywords_is_reserved;
use crate::signal::Signal;
use crate::wchar_ffi::{wcstring_list_ffi_t, WCharFromFFI, WCharToFFI};
use std::pin::Pin;
use std::sync::Arc;
struct FunctionCmdOpts {
print_help: bool,
shadow_scope: bool,
description: WString,
events: Vec<EventDescription>,
named_arguments: Vec<WString>,
inherit_vars: Vec<WString>,
wrap_targets: Vec<WString>,
}
impl Default for FunctionCmdOpts {
fn default() -> Self {
Self {
print_help: false,
shadow_scope: true,
description: WString::new(),
events: Vec::new(),
named_arguments: Vec::new(),
inherit_vars: Vec::new(),
wrap_targets: Vec::new(),
}
}
}
// This command is atypical in using the "-" (RETURN_IN_ORDER) option for flag parsing.
// This is needed due to the semantics of the -a/--argument-names flag.
const SHORT_OPTIONS: &wstr = L!("-:a:d:e:hj:p:s:v:w:SV:");
#[rustfmt::skip]
const LONG_OPTIONS: &[woption] = &[
wopt(L!("description"), woption_argument_t::required_argument, 'd'),
wopt(L!("on-signal"), woption_argument_t::required_argument, 's'),
wopt(L!("on-job-exit"), woption_argument_t::required_argument, 'j'),
wopt(L!("on-process-exit"), woption_argument_t::required_argument, 'p'),
wopt(L!("on-variable"), woption_argument_t::required_argument, 'v'),
wopt(L!("on-event"), woption_argument_t::required_argument, 'e'),
wopt(L!("wraps"), woption_argument_t::required_argument, 'w'),
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
wopt(L!("argument-names"), woption_argument_t::required_argument, 'a'),
wopt(L!("no-scope-shadowing"), woption_argument_t::no_argument, 'S'),
wopt(L!("inherit-variable"), woption_argument_t::required_argument, 'V'),
];
/// \return the internal_job_id for a pid, or None if none.
/// This looks through both active and finished jobs.
fn job_id_for_pid(pid: i32, parser: &parser_t) -> Option<u64> {
if let Some(job) = parser.job_get_from_pid(pid) {
Some(job.get_internal_job_id())
} else {
parser
.get_wait_handles()
.get_by_pid(pid)
.map(|h| h.internal_job_id)
}
}
/// Parses options to builtin function, populating opts.
/// Returns an exit status.
fn parse_cmd_opts(
opts: &mut FunctionCmdOpts,
optind: &mut usize,
argv: &mut [&wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Option<c_int> {
let cmd = L!("function");
let print_hints = false;
let mut handling_named_arguments = false;
let mut w = wgetopter_t::new(SHORT_OPTIONS, LONG_OPTIONS, argv);
while let Some(opt) = w.wgetopt_long() {
// NONOPTION_CHAR_CODE is returned when we reach a non-permuted non-option.
if opt != 'a' && opt != NONOPTION_CHAR_CODE {
handling_named_arguments = false;
}
match opt {
NONOPTION_CHAR_CODE => {
// A positional argument we got because we use RETURN_IN_ORDER.
let woptarg = w.woptarg.unwrap().to_owned();
if handling_named_arguments {
opts.named_arguments.push(woptarg);
} else {
streams.err.append(wgettext_fmt!(
"%ls: %ls: unexpected positional argument",
cmd,
woptarg
));
return STATUS_INVALID_ARGS;
}
}
'd' => {
opts.description = w.woptarg.unwrap().to_owned();
}
's' => {
let Some(signal) = Signal::parse(w.woptarg.unwrap()) else {
streams.err.append(wgettext_fmt!(
"%ls: Unknown signal '%ls'",
cmd,
w.woptarg.unwrap()
));
return STATUS_INVALID_ARGS;
};
opts.events.push(EventDescription::Signal { signal });
}
'v' => {
let name = w.woptarg.unwrap().to_owned();
if !valid_var_name(&name) {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_VARNAME, cmd, name));
return STATUS_INVALID_ARGS;
}
opts.events.push(EventDescription::Variable { name });
}
'e' => {
let param = w.woptarg.unwrap().to_owned();
opts.events.push(EventDescription::Generic { param });
}
'j' | 'p' => {
let woptarg = w.woptarg.unwrap();
let e: EventDescription;
if opt == 'j' && woptarg == "caller" {
let libdata = parser.ffi_libdata_pod_const();
let caller_id = if libdata.is_subshell {
libdata.caller_id
} else {
0
};
if caller_id == 0 {
streams.err.append(wgettext_fmt!(
"%ls: calling job for event handler not found",
cmd
));
return STATUS_INVALID_ARGS;
}
e = EventDescription::CallerExit { caller_id };
} else if opt == 'p' && woptarg == "%self" {
// Safety: getpid() is always successful.
let pid = unsafe { libc::getpid() };
e = EventDescription::ProcessExit { pid };
} else {
let pid = fish_wcstoi(woptarg);
if pid.is_err() || pid.unwrap() < 0 {
streams
.err
.append(wgettext_fmt!("%ls: %ls: invalid process id", cmd));
return STATUS_INVALID_ARGS;
}
let pid = pid.unwrap();
if opt == 'p' {
e = EventDescription::ProcessExit { pid };
} else {
// TODO: rationalize why a default of 0 is sensible.
let internal_job_id = job_id_for_pid(pid, parser).unwrap_or(0);
e = EventDescription::JobExit {
pid,
internal_job_id,
};
}
}
opts.events.push(e);
}
'a' => {
handling_named_arguments = true;
opts.named_arguments.push(w.woptarg.unwrap().to_owned());
}
'S' => {
opts.shadow_scope = false;
}
'w' => {
opts.wrap_targets.push(w.woptarg.unwrap().to_owned());
}
'V' => {
let woptarg = w.woptarg.unwrap();
if !valid_var_name(woptarg) {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_VARNAME, cmd, woptarg));
return STATUS_INVALID_ARGS;
}
opts.inherit_vars.push(woptarg.to_owned());
}
'h' => {
opts.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;
}
other => {
panic!("Unexpected retval from wgetopt_long: {}", other);
}
}
}
*optind = w.woptind;
STATUS_CMD_OK
}
fn validate_function_name(
argv: &mut [&wstr],
function_name: &mut WString,
cmd: &wstr,
streams: &mut io_streams_t,
) -> Option<c_int> {
if argv.len() < 2 {
// This is currently impossible but let's be paranoid.
streams
.err
.append(wgettext_fmt!("%ls: function name required", cmd));
return STATUS_INVALID_ARGS;
}
*function_name = argv[1].to_owned();
if !valid_func_name(function_name) {
streams.err.append(wgettext_fmt!(
"%ls: %ls: invalid function name",
cmd,
function_name,
));
return STATUS_INVALID_ARGS;
}
if parser_keywords_is_reserved(function_name) {
streams.err.append(wgettext_fmt!(
"%ls: %ls: cannot use reserved keyword as function name",
cmd,
function_name
));
return STATUS_INVALID_ARGS;
}
STATUS_CMD_OK
}
/// Define a function. Calls into `function.rs` to perform the heavy lifting of defining a
/// function. Note this isn't strictly a "builtin": it is called directly from parse_execution.
/// That is why its signature is different from the other builtins.
pub fn function(
parser: &mut parser_t,
streams: &mut io_streams_t,
c_args: &mut [&wstr],
func_node: NodeRef<BlockStatement>,
) -> Option<c_int> {
// The wgetopt function expects 'function' as the first argument. Make a new vec with
// that property. This is needed because this builtin has a different signature than the other
// builtins.
let mut args = vec![L!("function")];
args.extend_from_slice(c_args);
let argv: &mut [&wstr] = &mut args;
let cmd = argv[0];
// A valid function name has to be the first argument.
let mut function_name = WString::new();
let mut retval = validate_function_name(argv, &mut function_name, cmd, streams);
if retval != STATUS_CMD_OK {
return retval;
}
let argv = &mut argv[1..];
let mut opts = FunctionCmdOpts::default();
let mut optind = 0;
retval = parse_cmd_opts(&mut opts, &mut optind, argv, parser, streams);
if retval != STATUS_CMD_OK {
return retval;
}
if opts.print_help {
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_CMD_OK;
}
if argv.len() != optind {
if !opts.named_arguments.is_empty() {
// Remaining arguments are named arguments.
for &arg in argv[optind..].iter() {
if !valid_var_name(arg) {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_VARNAME, cmd, arg));
return STATUS_INVALID_ARGS;
}
opts.named_arguments.push(arg.to_owned());
}
} else {
streams.err.append(wgettext_fmt!(
"%ls: %ls: unexpected positional argument",
cmd,
argv[optind],
));
return STATUS_INVALID_ARGS;
}
}
// Extract the current filename.
let definition_file = unsafe { parser.pin().libdata().get_current_filename().as_ref() }
.map(|s| Arc::new(s.from_ffi()));
// Ensure inherit_vars is unique and then populate it.
opts.inherit_vars.sort_unstable();
opts.inherit_vars.dedup();
let inherit_vars: Vec<(WString, Vec<WString>)> = opts
.inherit_vars
.into_iter()
.filter_map(|name| {
let vals = parser.get_vars().get(&name)?.as_list().to_vec();
Some((name, vals))
})
.collect();
// We have what we need to actually define the function.
let props = function::FunctionProperties {
func_node,
named_arguments: opts.named_arguments,
description: opts.description,
inherit_vars: inherit_vars.into_boxed_slice(),
shadow_scope: opts.shadow_scope,
is_autoload: RelaxedAtomicBool::new(false),
definition_file,
is_copy: false,
copy_definition_file: None,
copy_definition_lineno: 0,
};
// Add the function itself.
function::add(function_name.clone(), Arc::new(props));
// Handle wrap targets by creating the appropriate completions.
for wt in opts.wrap_targets.into_iter() {
ffi::complete_add_wrapper(&function_name.to_ffi(), &wt.to_ffi());
}
// Add any event handlers.
for ed in &opts.events {
event::add_handler(EventHandler::new(ed.clone(), Some(function_name.clone())));
}
// If there is an --on-process-exit or --on-job-exit event handler for some pid, and that
// process has already exited, run it immediately (#7210).
for ed in &opts.events {
match *ed {
EventDescription::ProcessExit { pid } if pid != event::ANY_PID => {
if let Some(status) = parser
.get_wait_handles()
.get_by_pid(pid)
.and_then(|wh| wh.status())
{
event::fire(parser, event::Event::process_exit(pid, status));
}
}
EventDescription::JobExit { pid, .. } if pid != event::ANY_PID => {
if let Some(wh) = parser.get_wait_handles().get_by_pid(pid) {
if wh.is_completed() {
event::fire(parser, event::Event::job_exit(pid, wh.internal_job_id));
}
}
}
_ => (),
}
}
STATUS_CMD_OK
}
fn builtin_function_ffi(
parser: Pin<&mut parser_t>,
streams: Pin<&mut io_streams_ffi_t>,
c_args: &wcstring_list_ffi_t,
source_u8: *const u8, // unowned ParsedSourceRefFFI
func_node: &BlockStatement,
) -> i32 {
let storage = c_args.from_ffi();
let mut args = truncate_args_on_nul(&storage);
let node = unsafe {
let source_ref: &ParsedSourceRefFFI = &*(source_u8.cast());
NodeRef::from_parts(
source_ref
.0
.as_ref()
.expect("Should have parsed source")
.clone(),
func_node,
)
};
function(
parser.unpin(),
&mut io_streams_t::new(streams),
args.as_mut_slice(),
node,
)
.expect("function builtin should always return a non-None status")
}
#[cxx::bridge]
mod builtin_function {
extern "C++" {
include!("ast.h");
include!("parser.h");
include!("io.h");
type parser_t = crate::ffi::parser_t;
type io_streams_t = crate::ffi::io_streams_t;
type wcstring_list_ffi_t = crate::ffi::wcstring_list_ffi_t;
type BlockStatement = crate::ast::BlockStatement;
}
extern "Rust" {
fn builtin_function_ffi(
parser: Pin<&mut parser_t>,
streams: Pin<&mut io_streams_t>,
c_args: &wcstring_list_ffi_t,
source: *const u8, // unowned ParsedSourceRefFFI
func_node: &BlockStatement,
) -> i32;
}
}

View File

@@ -0,0 +1,422 @@
use super::prelude::*;
use crate::common::escape_string;
use crate::common::reformat_for_screen;
use crate::common::valid_func_name;
use crate::common::{EscapeFlags, EscapeStringStyle};
use crate::event::{self};
use crate::ffi::colorize_shell;
use crate::function;
use crate::parser_keywords::parser_keywords_is_reserved;
use crate::termsize::termsize_last;
struct FunctionsCmdOpts<'args> {
print_help: bool,
erase: bool,
list: bool,
show_hidden: bool,
query: bool,
copy: bool,
report_metadata: bool,
no_metadata: bool,
verbose: bool,
handlers: bool,
handlers_type: Option<&'args wstr>,
description: Option<&'args wstr>,
}
impl Default for FunctionsCmdOpts<'_> {
fn default() -> Self {
Self {
print_help: false,
erase: false,
list: false,
show_hidden: false,
query: false,
copy: false,
report_metadata: false,
no_metadata: false,
verbose: false,
handlers: false,
handlers_type: None,
description: None,
}
}
}
const NO_METADATA_SHORT: char = 2 as char;
const SHORT_OPTIONS: &wstr = L!(":Ht:Dacd:ehnqv");
#[rustfmt::skip]
const LONG_OPTIONS: &[woption] = &[
wopt(L!("erase"), woption_argument_t::no_argument, 'e'),
wopt(L!("description"), woption_argument_t::required_argument, 'd'),
wopt(L!("names"), woption_argument_t::no_argument, 'n'),
wopt(L!("all"), woption_argument_t::no_argument, 'a'),
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
wopt(L!("query"), woption_argument_t::no_argument, 'q'),
wopt(L!("copy"), woption_argument_t::no_argument, 'c'),
wopt(L!("details"), woption_argument_t::no_argument, 'D'),
wopt(L!("no-details"), woption_argument_t::no_argument, NO_METADATA_SHORT),
wopt(L!("verbose"), woption_argument_t::no_argument, 'v'),
wopt(L!("handlers"), woption_argument_t::no_argument, 'H'),
wopt(L!("handlers-type"), woption_argument_t::required_argument, 't'),
];
/// Parses options to builtin function, populating opts.
/// Returns an exit status.
fn parse_cmd_opts<'args>(
opts: &mut FunctionsCmdOpts<'args>,
optind: &mut usize,
argv: &mut [&'args wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Option<c_int> {
let cmd = L!("functions");
let print_hints = false;
let mut w = wgetopter_t::new(SHORT_OPTIONS, LONG_OPTIONS, argv);
while let Some(opt) = w.wgetopt_long() {
match opt {
'v' => opts.verbose = true,
'e' => opts.erase = true,
'D' => opts.report_metadata = true,
NO_METADATA_SHORT => opts.no_metadata = true,
'd' => {
opts.description = Some(w.woptarg.unwrap());
}
'n' => opts.list = true,
'a' => opts.show_hidden = true,
'h' => opts.print_help = true,
'q' => opts.query = true,
'c' => opts.copy = true,
'H' => opts.handlers = true,
't' => {
opts.handlers = true;
opts.handlers_type = Some(w.woptarg.unwrap());
}
':' => {
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;
}
other => {
panic!("Unexpected retval from wgetopt_long: {}", other);
}
}
}
*optind = w.woptind;
STATUS_CMD_OK
}
pub fn functions(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let cmd = args[0];
let mut opts = FunctionsCmdOpts::default();
let mut optind = 0;
let retval = parse_cmd_opts(&mut opts, &mut optind, args, parser, streams);
if retval != STATUS_CMD_OK {
return retval;
}
// Shadow our args with the positionals
let args = &args[optind..];
if opts.print_help {
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_CMD_OK;
}
let describe = opts.description.is_some();
if [describe, opts.erase, opts.list, opts.query, opts.copy]
.into_iter()
.filter(|b| *b)
.count()
> 1
{
streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_INVALID_ARGS;
}
if opts.report_metadata && opts.no_metadata {
streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_INVALID_ARGS;
}
if opts.erase {
for arg in args {
function::remove(arg);
}
// Historical - this never failed?
return STATUS_CMD_OK;
}
if let Some(desc) = opts.description {
if args.len() != 1 {
streams.err.append(wgettext_fmt!(
"%ls: Expected exactly one function name\n",
cmd
));
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_INVALID_ARGS;
}
let current_func = args[0];
if !function::exists(current_func, parser) {
streams.err.append(wgettext_fmt!(
"%ls: Function '%ls' does not exist\n",
cmd,
current_func
));
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_CMD_ERROR;
}
function::set_desc(current_func, desc.into(), parser);
return STATUS_CMD_OK;
}
if opts.report_metadata {
if args.len() != 1 {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
// This error is
// functions: --details: expected 1 arguments; got 2
// The "--details" was "argv[optind - 1]" in the C++
// which would just give the last option.
// This is broken because you could do `functions --details --verbose foo bar`, and it would error about "--verbose".
"--details",
1,
args.len()
));
return STATUS_INVALID_ARGS;
}
let props = function::get_props_autoload(args[0], parser);
let def_file = if let Some(p) = props.as_ref() {
if let Some(cpf) = &p.copy_definition_file {
cpf.as_ref().to_owned()
} else if let Some(df) = &p.definition_file {
df.as_ref().to_owned()
} else {
L!("stdin").to_owned()
}
} else {
L!("n/a").to_owned()
};
streams.out.appendln(def_file);
if opts.verbose {
let copy_place = match props.as_ref() {
Some(p) if p.copy_definition_file.is_some() => {
if let Some(df) = &p.definition_file {
df.as_ref().to_owned()
} else {
L!("stdin").to_owned()
}
}
Some(p) if p.is_autoload.load() => L!("autoloaded").to_owned(),
Some(p) if !p.is_autoload.load() => L!("not-autoloaded").to_owned(),
_ => L!("n/a").to_owned(),
};
streams.out.appendln(copy_place);
let line = if let Some(p) = props.as_ref() {
p.definition_lineno()
} else {
0
};
streams.out.appendln(line.to_wstring());
let shadow = match props.as_ref() {
Some(p) if p.shadow_scope => L!("scope-shadowing").to_owned(),
Some(p) if !p.shadow_scope => L!("no-scope-shadowing").to_owned(),
_ => L!("n/a").to_owned(),
};
streams.out.appendln(shadow);
let desc = match props.as_ref() {
Some(p) if !p.description.is_empty() => escape_string(
&p.description,
EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES | EscapeFlags::NO_QUOTED),
),
Some(p) if p.description.is_empty() => L!("").to_owned(),
_ => L!("n/a").to_owned(),
};
streams.out.appendln(desc);
}
// Historical - this never failed?
return STATUS_CMD_OK;
}
if opts.handlers {
// Empty handlers-type is the same as "all types".
if !opts.handlers_type.unwrap_or(L!("")).is_empty()
&& !event::EVENT_FILTER_NAMES.contains(&opts.handlers_type.unwrap())
{
streams.err.append(wgettext_fmt!(
"%ls: Expected generic | variable | signal | exit | job-id for --handlers-type\n",
cmd
));
return STATUS_INVALID_ARGS;
}
event::print(streams, opts.handlers_type.unwrap_or(L!("")));
return STATUS_CMD_OK;
}
if opts.query && args.is_empty() {
return STATUS_CMD_ERROR;
}
if opts.list || args.is_empty() {
let mut names = function::get_names(opts.show_hidden);
names.sort();
if streams.out_is_terminal() {
let mut buff = WString::new();
let mut first: bool = true;
for name in names {
if !first {
buff.push_utfstr(L!(", "));
}
buff.push_utfstr(&name);
first = false;
}
streams
.out
.append(reformat_for_screen(&buff, &termsize_last()));
} else {
for name in names {
streams.out.appendln(name);
}
}
return STATUS_CMD_OK;
}
if opts.copy {
if args.len() != 2 {
streams.err.append(wgettext_fmt!(
"%ls: Expected exactly two names (current function name, and new function name)\n",
cmd
));
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_INVALID_ARGS;
}
let current_func = args[0];
let new_func = args[1];
if !function::exists(current_func, parser) {
streams.err.append(wgettext_fmt!(
"%ls: Function '%ls' does not exist\n",
cmd,
current_func
));
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_CMD_ERROR;
}
if !valid_func_name(new_func) || parser_keywords_is_reserved(new_func) {
streams.err.append(wgettext_fmt!(
"%ls: Illegal function name '%ls'\n",
cmd,
new_func
));
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_INVALID_ARGS;
}
if function::exists(new_func, parser) {
streams.err.append(wgettext_fmt!(
"%ls: Function '%ls' already exists. Cannot create copy '%ls'\n",
cmd,
new_func,
current_func
));
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_CMD_ERROR;
}
if function::copy(current_func, new_func.into(), parser) {
return STATUS_CMD_OK;
}
return STATUS_CMD_ERROR;
}
let mut res: c_int = STATUS_CMD_OK.unwrap();
let mut first = true;
for arg in args.iter() {
let Some(props) = function::get_props_autoload(arg, parser) else {
res += 1;
first = false;
continue;
};
if opts.query {
continue;
}
if !first {
streams.out.append(L!("\n"));
};
let mut comment = WString::new();
if !opts.no_metadata {
// TODO: This is duplicated in type.
// Extract this into a helper.
match props.definition_file() {
Some(path) if path == "-" => {
comment.push_utfstr(&wgettext!("Defined via `source`"))
}
Some(path) => {
comment.push_utfstr(&wgettext_fmt!(
"Defined in %ls @ line %d",
path,
props.definition_lineno()
));
}
None => comment.push_utfstr(&wgettext_fmt!("Defined interactively")),
}
if props.is_copy() {
match props.copy_definition_file() {
Some(path) if path == "-" => {
comment.push_utfstr(&wgettext_fmt!(", copied via `source`"))
}
Some(path) => {
comment.push_utfstr(&wgettext_fmt!(
", copied in %ls @ line %d",
path,
props.copy_definition_lineno()
));
}
None => comment.push_utfstr(&wgettext_fmt!(", copied interactively")),
}
}
}
let mut def = WString::new();
if !comment.is_empty() {
def.push_utfstr(&sprintf!(
"# %ls\n%ls",
comment,
props.annotated_definition(arg)
));
} else {
def = props.annotated_definition(arg);
}
if streams.out_is_terminal() {
let col = colorize_shell(&def.to_ffi(), parser.pin()).from_ffi();
streams.out.append(col);
} else {
streams.out.append(def);
}
first = false;
}
return Some(res);
}

View File

@@ -0,0 +1,255 @@
use super::prelude::*;
use crate::tinyexpr::te_interp;
/// The maximum number of points after the decimal that we'll print.
const DEFAULT_SCALE: usize = 6;
/// The end of the range such that every integer is representable as a double.
/// i.e. this is the first value such that x + 1 == x (or == x + 2, depending on rounding mode).
const MAX_CONTIGUOUS_INTEGER: f64 = (1_u64 << f64::MANTISSA_DIGITS) as f64;
struct Options {
print_help: bool,
scale: usize,
base: usize,
}
#[widestrs]
fn parse_cmd_opts(
args: &mut [&wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Result<(Options, usize), Option<c_int>> {
const cmd: &wstr = "math"L;
let print_hints = true;
// This command is atypical in using the "+" (REQUIRE_ORDER) option for flag parsing.
// This is needed because of the minus, `-`, operator in math expressions.
const SHORT_OPTS: &wstr = "+:hs:b:"L;
const LONG_OPTS: &[woption] = &[
wopt("scale"L, woption_argument_t::required_argument, 's'),
wopt("base"L, woption_argument_t::required_argument, 'b'),
wopt("help"L, woption_argument_t::no_argument, 'h'),
];
let mut opts = Options {
print_help: false,
scale: DEFAULT_SCALE,
base: 10,
};
let mut have_scale = false;
let mut w = wgetopter_t::new(SHORT_OPTS, LONG_OPTS, args);
while let Some(c) = w.wgetopt_long() {
match c {
's' => {
let optarg = w.woptarg.unwrap();
have_scale = true;
// "max" is the special value that tells us to pick the maximum scale.
if optarg == "max" {
opts.scale = 15;
} else {
let scale = fish_wcstoi(optarg);
if scale.is_err() || scale.unwrap() < 0 || scale.unwrap() > 15 {
streams.err.append(wgettext_fmt!(
"%ls: %ls: invalid base value\n",
cmd,
optarg
));
return Err(STATUS_INVALID_ARGS);
}
// We know the value is in the range [0, 15]
opts.scale = scale.unwrap() as usize;
}
}
'b' => {
let optarg = w.woptarg.unwrap();
if optarg == "hex" {
opts.base = 16;
} else if optarg == "octal" {
opts.base = 8;
} else {
let base = fish_wcstoi(optarg);
if base.is_err() || (base.unwrap() != 8 && base.unwrap() != 16) {
streams.err.append(wgettext_fmt!(
"%ls: %ls: invalid base value\n",
cmd,
optarg
));
return Err(STATUS_INVALID_ARGS);
}
// We know the value is 8 or 16.
opts.base = base.unwrap() as usize;
}
}
'h' => {
opts.print_help = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.woptind - 1], print_hints);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
// For most commands this is an error. We ignore it because a math expression
// can begin with a minus sign.
return Ok((opts, w.woptind - 1));
}
_ => {
panic!("unexpected retval from wgeopter.next()");
}
}
}
if have_scale && opts.scale != 0 && opts.base != 10 {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
"non-zero scale value only valid
for base 10"
));
return Err(STATUS_INVALID_ARGS);
}
Ok((opts, w.woptind))
}
/// Return a formatted version of the value `v` respecting the given `opts`.
fn format_double(mut v: f64, opts: &Options) -> WString {
if opts.base == 16 {
v = v.trunc();
let mneg = if v.is_sign_negative() { "-" } else { "" };
return sprintf!("%s0x%lx", mneg, v.abs() as u64);
} else if opts.base == 8 {
v = v.trunc();
if v == 0.0 {
// not 00
return WString::from_str("0");
}
let mneg = if v.is_sign_negative() { "-" } else { "" };
return sprintf!("%s0%lo", mneg, v.abs() as u64);
}
// As a special-case, a scale of 0 means to truncate to an integer
// instead of rounding.
if opts.scale == 0 {
v = v.trunc();
return sprintf!("%.*f", opts.scale, v);
}
let mut ret = sprintf!("%.*f", opts.scale, v);
// If we contain a decimal separator, trim trailing zeros after it, and then the separator
// itself if there's nothing after it. Detect a decimal separator as a non-digit.
if ret.chars().any(|c| !c.is_ascii_digit()) {
let trailing_zeroes = ret.chars().rev().take_while(|&c| c == '0').count();
let mut to_keep = ret.len() - trailing_zeroes;
if ret.as_char_slice()[to_keep - 1] == '.' {
to_keep -= 1;
}
ret.truncate(to_keep);
}
// If we trimmed everything it must have just been zero.
// TODO: can this ever happen?
if ret.is_empty() {
ret.push('0');
}
ret
}
#[widestrs]
fn evaluate_expression(
cmd: &wstr,
streams: &mut io_streams_t,
opts: &Options,
expression: &wstr,
) -> Option<c_int> {
let ret = te_interp(expression);
match ret {
Ok(n) => {
// Check some runtime errors after the fact.
// TODO: Really, this should be done in tinyexpr
// (e.g. infinite is the result of "x / 0"),
// but that's much more work.
let error_message = if n.is_infinite() {
"Result is infinite"L
} else if n.is_nan() {
"Result is not a number"L
} else if n.abs() >= MAX_CONTIGUOUS_INTEGER {
"Result magnitude is too large"L
} else {
let mut s = format_double(n, opts);
s.push('\n');
streams.out.append(s);
return STATUS_CMD_OK;
};
streams
.err
.append(sprintf!("%ls: Error: %ls\n"L, cmd, error_message));
streams.err.append(sprintf!("'%ls'\n"L, expression));
STATUS_CMD_ERROR
}
Err(err) => {
streams.err.append(sprintf!(
"%ls: Error: %ls\n"L,
cmd,
err.kind.describe_wstr()
));
streams.err.append(sprintf!("'%ls'\n"L, expression));
let padding = WString::from_chars(vec![' '; err.position + 1]);
if err.len >= 2 {
let tildes = WString::from_chars(vec!['~'; err.len - 2]);
streams.err.append(sprintf!("%ls^%ls^\n"L, padding, tildes));
} else {
streams.err.append(sprintf!("%ls^\n"L, padding));
}
STATUS_CMD_ERROR
}
}
}
/// How much math reads at one. We don't expect very long input.
const MATH_CHUNK_SIZE: usize = 1024;
/// The math builtin evaluates math expressions.
#[widestrs]
pub fn math(
parser: &mut parser_t,
streams: &mut io_streams_t,
argv: &mut [&wstr],
) -> Option<c_int> {
let cmd = argv[0];
let (opts, mut optind) = match parse_cmd_opts(argv, parser, streams) {
Ok(x) => x,
Err(e) => return e,
};
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
let mut expression = WString::new();
for (arg, _) in Arguments::new(argv, &mut optind, streams, MATH_CHUNK_SIZE) {
if !expression.is_empty() {
expression.push(' ')
}
expression.push_utfstr(&arg);
}
if expression.is_empty() {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1, 0));
return STATUS_CMD_ERROR;
}
evaluate_expression(cmd, streams, &opts, &expression)
}

View File

@@ -0,0 +1,49 @@
pub mod shared;
pub mod abbr;
pub mod argparse;
pub mod bg;
pub mod block;
pub mod builtin;
pub mod cd;
pub mod command;
pub mod contains;
pub mod count;
pub mod echo;
pub mod emit;
pub mod exit;
pub mod function;
pub mod functions;
pub mod math;
pub mod path;
pub mod printf;
pub mod pwd;
pub mod random;
pub mod realpath;
pub mod r#return;
pub mod set_color;
pub mod status;
pub mod string;
pub mod test;
pub mod r#type;
pub mod wait;
// Note these tests will NOT run with cfg(test).
mod tests;
mod prelude {
pub use super::shared::*;
pub use libc::c_int;
pub(crate) use crate::{
ffi::{self, parser_t, separation_type_t, Repin},
wchar::prelude::*,
wchar_ffi::{c_str, AsWstr, WCharFromFFI, WCharToFFI},
wgetopt::{
wgetopter_t, wopt, woption,
woption_argument_t::{self, *},
NONOPTION_CHAR_CODE,
},
wutil::{fish_wcstoi, fish_wcstol, fish_wcstoul},
};
}

View File

@@ -0,0 +1,981 @@
use crate::env::environment::Environment;
use std::cmp::Ordering;
use std::os::unix::prelude::{FileTypeExt, MetadataExt};
use std::time::SystemTime;
use super::prelude::*;
use crate::path::path_apply_working_directory;
use crate::util::wcsfilecmp_glob;
use crate::wcstringutil::split_string_tok;
use crate::wutil::{
file_id_for_path, lwstat, normalize_path, waccess, wbasename, wdirname, wrealpath, wstat,
INVALID_FILE_ID,
};
use bitflags::bitflags;
use libc::{getegid, geteuid, mode_t, uid_t, F_OK, PATH_MAX, R_OK, S_ISGID, S_ISUID, W_OK, X_OK};
use super::shared::BuiltinCmd;
macro_rules! path_error {
(
$streams:expr,
$string:expr
$(, $args:expr)+
$(,)?
) => {
$streams.err.append(L!("path "));
$streams.err.append(wgettext_fmt!($string, $($args),*));
};
}
fn path_unknown_option(
parser: &mut parser_t,
streams: &mut io_streams_t,
subcmd: &wstr,
opt: &wstr,
) {
path_error!(streams, BUILTIN_ERR_UNKNOWN, subcmd, opt);
builtin_print_error_trailer(parser, streams, L!("path"));
}
// How many bytes we read() at once.
// We use PATH_MAX here so we always get at least one path,
// and so we can automatically detect NULL-separated input.
const PATH_CHUNK_SIZE: usize = PATH_MAX as usize;
#[inline]
fn arguments<'iter, 'args>(
args: &'iter [&'args wstr],
optind: &'iter mut usize,
streams: &mut io_streams_t,
) -> Arguments<'args, 'iter> {
Arguments::new(args, optind, streams, PATH_CHUNK_SIZE)
}
bitflags! {
#[derive(Default)]
pub struct TypeFlags: u32 {
/// A block device
const BLOCK = 1 << 0;
/// A directory
const DIR = 1 << 1;
/// A regular file
const FILE = 1 << 2;
/// A link
const LINK = 1 << 3;
/// A character device
const CHAR = 1 << 4;
/// A fifo
const FIFO = 1 << 5;
/// A socket
const SOCK = 1 << 6;
}
}
impl TryFrom<&wstr> for TypeFlags {
type Error = ();
fn try_from(value: &wstr) -> Result<Self, Self::Error> {
let flag = match value {
t if t == "file" => Self::FILE,
t if t == "dir" => Self::DIR,
t if t == "block" => Self::BLOCK,
t if t == "char" => Self::CHAR,
t if t == "fifo" => Self::FIFO,
t if t == "socket" => Self::SOCK,
t if t == "link" => Self::LINK,
_ => return Err(()),
};
Ok(flag)
}
}
bitflags! {
#[derive(Default)]
pub struct PermFlags: u32 {
const READ = 1 << 0;
const WRITE = 1 << 1;
const EXEC = 1 << 2;
const SUID = 1 << 3;
const SGID = 1 << 4;
const USER = 1 << 5;
const GROUP = 1 << 6;
}
}
impl PermFlags {
fn is_special(self) -> bool {
self.intersects(Self::SUID | Self::SGID | Self::USER | Self::GROUP)
}
}
impl TryFrom<&wstr> for PermFlags {
type Error = ();
fn try_from(value: &wstr) -> Result<Self, Self::Error> {
let flag = match value {
t if t == "read" => Self::READ,
t if t == "write" => Self::WRITE,
t if t == "exec" => Self::EXEC,
t if t == "suid" => Self::SUID,
t if t == "sgid" => Self::SGID,
t if t == "user" => Self::USER,
t if t == "group" => Self::GROUP,
_ => return Err(()),
};
Ok(flag)
}
}
/// This is used by the subcommands to communicate with the option parser which flags are
/// valid and get the result of parsing the command for flags.
#[derive(Default)]
struct Options<'args> {
null_in: bool,
null_out: bool,
quiet: bool,
invert_valid: bool,
invert: bool,
relative_valid: bool,
relative: bool,
reverse_valid: bool,
reverse: bool,
unique_valid: bool,
unique: bool,
key: Option<&'args wstr>,
types_valid: bool,
types: Option<TypeFlags>,
perms_valid: bool,
perms: Option<PermFlags>,
arg1: Option<&'args wstr>,
}
#[inline]
fn path_out(streams: &mut io_streams_t, opts: &Options<'_>, s: impl AsRef<wstr>) {
let s = s.as_ref();
if !opts.quiet {
if !opts.null_out {
streams
.out
.append_with_separation(s, separation_type_t::explicitly, true);
} else {
let mut output = WString::with_capacity(s.len() + 1);
output.push_utfstr(s);
output.push('\0');
streams.out.append(output);
}
}
}
fn construct_short_opts(opts: &Options) -> WString {
// All commands accept -z, -Z and -q
let mut short_opts = WString::from(":zZq");
if opts.perms_valid {
short_opts += L!("p:");
short_opts += L!("rwx");
}
if opts.types_valid {
short_opts += L!("t:");
short_opts += L!("fld");
}
if opts.invert_valid {
short_opts.push('v');
}
if opts.relative_valid {
short_opts.push('R');
}
if opts.reverse_valid {
short_opts.push('r');
}
if opts.unique_valid {
short_opts.push('u');
}
short_opts
}
/// Note that several long flags share the same short flag. That is okay. The caller is expected
/// to indicate that a max of one of the long flags sharing a short flag is valid.
/// Remember: adjust the completions in share/completions/ when options change
const LONG_OPTIONS: [woption<'static>; 10] = [
wopt(L!("quiet"), no_argument, 'q'),
wopt(L!("null-in"), no_argument, 'z'),
wopt(L!("null-out"), no_argument, 'Z'),
wopt(L!("perm"), required_argument, 'p'),
wopt(L!("type"), required_argument, 't'),
wopt(L!("invert"), no_argument, 'v'),
wopt(L!("relative"), no_argument, 'R'),
wopt(L!("reverse"), no_argument, 'r'),
wopt(L!("unique"), no_argument, 'u'),
wopt(L!("key"), required_argument, NONOPTION_CHAR_CODE),
];
fn parse_opts<'args>(
opts: &mut Options<'args>,
optind: &mut usize,
n_req_args: usize,
args: &mut [&'args wstr],
parser: &mut parser_t,
streams: &mut io_streams_t,
) -> Option<c_int> {
let cmd = args[0];
let mut args_read = Vec::with_capacity(args.len());
args_read.extend_from_slice(args);
let short_opts = construct_short_opts(opts);
let mut w = wgetopter_t::new(&short_opts, &LONG_OPTIONS, args);
while let Some(c) = w.wgetopt_long() {
match c {
':' => {
streams.err.append(L!("path ")); // clone of string_error
builtin_missing_argument(parser, streams, cmd, args_read[w.woptind - 1], false);
return STATUS_INVALID_ARGS;
}
'?' => {
path_unknown_option(parser, streams, cmd, args_read[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
'q' => {
opts.quiet = true;
continue;
}
'z' => {
opts.null_in = true;
continue;
}
'Z' => {
opts.null_out = true;
continue;
}
'v' if opts.invert_valid => {
opts.invert = true;
continue;
}
't' if opts.types_valid => {
let types = opts.types.get_or_insert_with(TypeFlags::default);
let types_args = split_string_tok(w.woptarg.unwrap(), L!(","), None);
for t in types_args {
let Ok(r#type) = t.try_into() else {
path_error!(streams, "%ls: Invalid type '%ls'\n", "path", t);
return STATUS_INVALID_ARGS;
};
*types |= r#type;
}
continue;
}
'p' if opts.perms_valid => {
let perms = opts.perms.get_or_insert_with(PermFlags::default);
let perms_args = split_string_tok(w.woptarg.unwrap(), L!(","), None);
for p in perms_args {
let Ok(perm) = p.try_into() else {
path_error!(streams, "%ls: Invalid permission '%ls'\n", "path", p);
return STATUS_INVALID_ARGS;
};
*perms |= perm;
}
continue;
}
'r' if opts.reverse_valid => {
opts.reverse = true;
continue;
}
'r' if opts.perms_valid => {
let perms = opts.perms.get_or_insert_with(PermFlags::default);
*perms |= PermFlags::READ;
continue;
}
'w' if opts.perms_valid => {
let perms = opts.perms.get_or_insert_with(PermFlags::default);
*perms |= PermFlags::WRITE;
continue;
}
'x' if opts.perms_valid => {
let perms = opts.perms.get_or_insert_with(PermFlags::default);
*perms |= PermFlags::EXEC;
continue;
}
'f' if opts.types_valid => {
let types = opts.types.get_or_insert_with(TypeFlags::default);
*types |= TypeFlags::FILE;
continue;
}
'l' if opts.types_valid => {
let types = opts.types.get_or_insert_with(TypeFlags::default);
*types |= TypeFlags::LINK;
continue;
}
'd' if opts.types_valid => {
let types = opts.types.get_or_insert_with(TypeFlags::default);
*types |= TypeFlags::DIR;
continue;
}
'u' if opts.unique_valid => {
opts.unique = true;
continue;
}
'R' if opts.relative_valid => {
opts.relative = true;
continue;
}
NONOPTION_CHAR_CODE => {
assert!(w.woptarg.is_some());
opts.key = w.woptarg;
continue;
}
_ => {
path_unknown_option(parser, streams, cmd, args_read[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
}
}
*optind = w.woptind;
if n_req_args != 0 {
assert!(n_req_args == 1);
opts.arg1 = args.get(*optind).copied();
if opts.arg1.is_some() {
*optind += 1;
}
if opts.arg1.is_none() && n_req_args == 1 {
path_error!(streams, BUILTIN_ERR_ARG_COUNT0, cmd);
return STATUS_INVALID_ARGS;
}
}
// At this point we should not have optional args and be reading args from stdin.
if streams.stdin_is_directly_redirected() && args.len() > *optind {
path_error!(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd);
return STATUS_INVALID_ARGS;
}
STATUS_CMD_OK
}
fn path_transform(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
func: impl Fn(&wstr) -> WString,
) -> Option<c_int> {
let mut opts = Options::default();
let mut optind = 0;
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
if retval != STATUS_CMD_OK {
return retval;
}
let mut n_transformed = 0;
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
true => SplitBehavior::Null,
false => SplitBehavior::InferNull,
});
for (arg, _) in arguments {
// Empty paths make no sense, but e.g. wbasename returns true for them.
if arg.is_empty() {
continue;
}
let transformed = func(&arg);
if transformed != arg {
n_transformed += 1;
// Return okay if path wasn't already in this form
// TODO: Is that correct?
if opts.quiet {
return STATUS_CMD_OK;
};
}
path_out(streams, &opts, transformed);
}
if n_transformed > 0 {
STATUS_CMD_OK
} else {
STATUS_CMD_ERROR
}
}
fn path_basename(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
path_transform(parser, streams, args, |s| wbasename(s).to_owned())
}
fn path_dirname(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
path_transform(parser, streams, args, |s| wdirname(s).to_owned())
}
fn normalize_help(path: &wstr) -> WString {
let mut np = normalize_path(path, false);
if !np.is_empty() && np.char_at(0) == '-' {
np = "./".chars().chain(np.chars()).collect();
}
np
}
fn path_normalize(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
path_transform(parser, streams, args, normalize_help)
}
fn path_mtime(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let mut opts = Options::default();
opts.relative_valid = true;
let mut optind = 0;
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
if retval != STATUS_CMD_OK {
return retval;
}
let mut n_transformed = 0;
let t = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
Ok(dur) => dur.as_secs() as i64,
Err(err) => -(err.duration().as_secs() as i64),
};
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
true => SplitBehavior::Null,
false => SplitBehavior::InferNull,
});
for (arg, _) in arguments {
let ret = file_id_for_path(&arg);
if ret != INVALID_FILE_ID {
if opts.quiet {
return STATUS_CMD_OK;
}
n_transformed += 1;
if !opts.relative {
path_out(streams, &opts, (ret.mod_seconds).to_wstring());
} else {
// note that the mod time can actually be before the system time
// so this can end up negative
#[allow(clippy::unnecessary_cast)]
path_out(streams, &opts, (t - ret.mod_seconds as i64).to_wstring());
}
}
}
if n_transformed > 0 {
STATUS_CMD_OK
} else {
STATUS_CMD_ERROR
}
}
fn find_extension(path: &wstr) -> Option<usize> {
// The extension belongs to the basename,
// if there is a "." before the last component it doesn't matter.
// e.g. ~/.config/fish/conf.d/foo
// does not have an extension! The ".d" here is not a file extension for "foo".
// And "~/.config" doesn't have an extension either - the ".config" is the filename.
let filename = wbasename(path);
// "." and ".." aren't really *files* and therefore don't have an extension.
if filename == "." || filename == ".." {
return None;
}
// If we don't have a "." or the "." is the first in the filename,
// we do not have an extension
let pos = filename.chars().rposition(|c| c == '.');
match pos {
None | Some(0) => None,
// Convert pos back to what it would be in the original path.
Some(pos) => Some(pos + path.len() - filename.len()),
}
}
#[test]
fn test_find_extension() {
let cases = [
(L!("foo.wmv"), Some(3)),
(L!("verylongfilename.wmv"), Some("verylongfilename".len())),
(L!("foo"), None),
(L!(".foo"), None),
(L!("./foo.wmv"), Some(5)),
];
for (f, ext_idx) in cases {
assert_eq!(find_extension(f), ext_idx);
}
}
fn path_extension(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let mut opts = Options::default();
let mut optind = 0;
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
if retval != STATUS_CMD_OK {
return retval;
}
let mut n_transformed = 0;
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
true => SplitBehavior::Null,
false => SplitBehavior::InferNull,
});
for (arg, _) in arguments {
let pos = find_extension(&arg);
let Some(pos) = pos else {
// If there is no extension the extension is empty.
// This is unambiguous because we include the ".".
path_out(streams, &opts, L!(""));
continue;
};
let ext = arg.slice_from(pos);
if opts.quiet && !ext.is_empty() {
return STATUS_CMD_OK;
}
path_out(streams, &opts, ext);
n_transformed += 1;
}
if n_transformed > 0 {
STATUS_CMD_OK
} else {
STATUS_CMD_ERROR
}
}
fn path_change_extension(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let mut opts = Options::default();
let mut optind = 0;
let retval = parse_opts(&mut opts, &mut optind, 1, args, parser, streams);
if retval != STATUS_CMD_OK {
return retval;
}
let mut n_transformed = 0usize;
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
true => SplitBehavior::Null,
false => SplitBehavior::InferNull,
});
for (mut arg, _) in arguments {
let pos = find_extension(&arg);
let mut ext = match pos {
Some(pos) => {
arg.to_mut().truncate(pos);
arg.into_owned()
}
None => arg.into_owned(),
};
// Only add on the extension "." if we have something.
// That way specifying an empty extension strips it.
if let Some(replacement) = opts.arg1 {
if !replacement.is_empty() {
if replacement.char_at(0) != '.' {
ext.push('.');
}
ext.push_utfstr(replacement);
}
}
path_out(streams, &opts, ext);
n_transformed += 1;
}
if n_transformed > 0 {
STATUS_CMD_OK
} else {
STATUS_CMD_ERROR
}
}
fn path_resolve(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let mut opts = Options::default();
let mut optind = 0;
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
if retval != STATUS_CMD_OK {
return retval;
}
let mut n_transformed = 0usize;
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
true => SplitBehavior::Null,
false => SplitBehavior::InferNull,
});
for (arg, _) in arguments {
let mut real = match wrealpath(&arg) {
Some(p) => p,
None => {
// The path doesn't exist, isn't readable or a symlink loop.
// We go up until we find something that works.
let mut next = arg.into_owned();
// First add $PWD if we're relative
if !next.is_empty() && next.char_at(0) != '/' {
next = path_apply_working_directory(&next, &parser.get_vars().get_pwd_slash());
}
let mut rest = wbasename(&next).to_owned();
let mut real = None;
while !next.is_empty() && next != "/" {
next = wdirname(&next).to_owned();
real = wrealpath(&next);
if let Some(ref mut real) = real {
real.push('/');
real.push_utfstr(&rest);
*real = normalize_path(real, false);
break;
}
rest = (wbasename(&next).to_owned() + L!("/")) + rest.as_utfstr();
}
match real {
Some(p) => p,
None => continue,
}
}
};
// Normalize the path so "../" components are eliminated even after
// nonexistent or non-directory components.
// Otherwise `path resolve foo/../` will be `$PWD/foo/../` if foo is a file.
real = normalize_path(&real, false);
// Return 0 if we found a realpath.
if opts.quiet {
return STATUS_CMD_OK;
}
path_out(streams, &opts, real);
n_transformed += 1;
}
if n_transformed > 0 {
STATUS_CMD_OK
} else {
STATUS_CMD_ERROR
}
}
fn path_sort(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let mut opts = Options::default();
opts.reverse_valid = true;
opts.unique_valid = true;
let mut optind = 0;
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
if retval != STATUS_CMD_OK {
return retval;
}
let keyfunc: &dyn Fn(&wstr) -> &wstr = match &opts.key {
Some(k) if k == "basename" => &wbasename as _,
Some(k) if k == "dirname" => &wdirname as _,
Some(k) if k == "path" => {
// Act as if --key hadn't been given.
opts.key = None;
&wbasename as _
}
None => &wbasename as _,
Some(k) => {
path_error!(streams, "%ls: Invalid sort key '%ls'\n", args[0], k);
return STATUS_INVALID_ARGS;
}
};
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
true => SplitBehavior::Null,
false => SplitBehavior::InferNull,
});
let mut list: Vec<_> = arguments.map(|(f, _)| f).collect();
if opts.key.is_some() {
// We use a stable sort here
list.sort_by(|a, b| {
match wcsfilecmp_glob(keyfunc(a), keyfunc(b)) {
// to avoid changing the order so we can chain calls
Ordering::Equal => Ordering::Greater,
order if opts.reverse => order.reverse(),
order => order,
}
});
if opts.unique {
// we are sorted, dedup will remove all duplicates
list.dedup_by(|a, b| keyfunc(a) == keyfunc(b));
}
} else {
// Without --key, we just sort by the entire path,
// so we have no need to transform and such.
list.sort_by(|a, b| {
match wcsfilecmp_glob(a, b) {
// to avoid changing the order so we can chain calls
Ordering::Equal => Ordering::Greater,
order if opts.reverse => order.reverse(),
order => order,
}
});
if opts.unique {
// we are sorted, dedup will remove all duplicates
list.dedup();
}
}
for entry in list {
path_out(streams, &opts, &entry);
}
/* TODO: Return true only if already sorted? */
STATUS_CMD_OK
}
fn filter_path(opts: &Options, path: &wstr) -> bool {
// TODO: Add moar stuff:
// fifos, sockets, size greater than zero, setuid, ...
// Nothing to check, file existence is checked elsewhere.
if opts.types.is_none() && opts.perms.is_none() {
return true;
}
if let Some(t) = opts.types {
let mut type_ok = false;
if t.contains(TypeFlags::LINK) {
let md = lwstat(path);
type_ok = md.is_ok() && md.unwrap().is_symlink();
}
let Ok(md) = wstat(path) else {
// Does not exist
return false;
};
let ft = md.file_type();
type_ok = match type_ok {
true => true,
_ if t.contains(TypeFlags::FILE) && ft.is_file() => true,
_ if t.contains(TypeFlags::DIR) && ft.is_dir() => true,
_ if t.contains(TypeFlags::BLOCK) && ft.is_block_device() => true,
_ if t.contains(TypeFlags::CHAR) && ft.is_char_device() => true,
_ if t.contains(TypeFlags::FIFO) && ft.is_fifo() => true,
_ if t.contains(TypeFlags::SOCK) && ft.is_socket() => true,
_ => false,
};
if !type_ok {
return false;
}
}
if let Some(perm) = opts.perms {
let mut amode = 0;
// TODO: Update bitflags so this works
/*
for f in perm {
amode |= match f {
PermFlags::READ => R_OK,
PermFlags::WRITE => W_OK,
PermFlags::EXEC => X_OK,
_ => PermFlags::empty(),
}
}
*/
if perm.contains(PermFlags::READ) {
amode |= R_OK;
}
if perm.contains(PermFlags::WRITE) {
amode |= W_OK;
}
if perm.contains(PermFlags::EXEC) {
amode |= X_OK;
}
// access returns 0 on success,
// -1 on failure. Yes, C can't even keep its bools straight.
if waccess(path, amode) != 0 {
return false;
}
// Permissions that require special handling
if perm.is_special() {
let Ok(md) = wstat(path) else {
// Does not exist, even though we just checked we can access it
// likely some kind of race condition
// We might want to warn the user about this?
return false;
};
#[allow(clippy::if_same_then_else)]
if perm.contains(PermFlags::SUID) && (md.mode() as mode_t & S_ISUID) == 0 {
return false;
} else if perm.contains(PermFlags::SGID) && (md.mode() as mode_t & S_ISGID) == 0 {
return false;
} else if perm.contains(PermFlags::USER) && (unsafe { geteuid() } != md.uid() as uid_t)
{
return false;
} else if perm.contains(PermFlags::GROUP) && (unsafe { getegid() } != md.gid() as uid_t)
{
return false;
}
}
}
// No filters failed.
true
}
fn path_filter_maybe_is(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
is_is: bool,
) -> Option<c_int> {
let mut opts = Options::default();
opts.types_valid = true;
opts.perms_valid = true;
opts.invert_valid = true;
let mut optind = 0;
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
if retval != STATUS_CMD_OK {
return retval;
}
// If we have been invoked as "path is", which is "path filter -q".
if is_is {
opts.quiet = true;
}
let mut n_transformed = 0;
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
true => SplitBehavior::Null,
false => SplitBehavior::InferNull,
});
for (arg, _) in arguments.filter(|(f, _)| {
(opts.perms.is_none() && opts.types.is_none()) || (filter_path(&opts, f) != opts.invert)
}) {
// If we don't have filters, check if it exists.
if opts.perms.is_none() && opts.types.is_none() {
let ok = waccess(&arg, F_OK) == 0;
if ok == opts.invert {
continue;
}
}
// We *know* this is a filename,
// and so if it starts with a `-` we *know* it is relative
// to $PWD. So we can add `./`.
// Empty paths make no sense, but e.g. wbasename returns true for them.
if !arg.is_empty() && arg.starts_with('-') {
let out = WString::from("./") + arg.as_ref();
path_out(streams, &opts, out);
} else {
path_out(streams, &opts, arg);
}
n_transformed += 1;
if opts.quiet {
return STATUS_CMD_OK;
};
}
if n_transformed > 0 {
STATUS_CMD_OK
} else {
STATUS_CMD_ERROR
}
}
fn path_filter(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
path_filter_maybe_is(parser, streams, args, false)
}
fn path_is(parser: &mut parser_t, streams: &mut io_streams_t, args: &mut [&wstr]) -> Option<c_int> {
path_filter_maybe_is(parser, streams, args, true)
}
/// The path builtin, for handling paths.
pub fn path(
parser: &mut parser_t,
streams: &mut io_streams_t,
args: &mut [&wstr],
) -> Option<c_int> {
let cmd = args[0];
let argc = args.len();
if argc <= 1 {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_MISSING_SUBCMD, cmd));
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_INVALID_ARGS;
}
if args[1] == "-h" || args[1] == "--help" {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
let subcmd_name = args[1];
let subcmd: BuiltinCmd = match subcmd_name.to_string().as_str() {
"basename" => path_basename,
"change-extension" => path_change_extension,
"dirname" => path_dirname,
"extension" => path_extension,
"filter" => path_filter,
"is" => path_is,
"mtime" => path_mtime,
"normalize" => path_normalize,
"resolve" => path_resolve,
"sort" => path_sort,
_ => {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_INVALID_SUBCMD, cmd, subcmd_name));
builtin_print_error_trailer(parser, streams, cmd);
return STATUS_INVALID_ARGS;
}
};
if argc >= 3 && (args[2] == "-h" || args[2] == "--help") {
// Unlike string, we don't have separate docs (yet)
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
let args = &mut args[1..];
return subcmd(parser, streams, args);
}

View File

@@ -0,0 +1,803 @@
// printf - format and print data
// Copyright (C) 1990-2007 Free Software Foundation, Inc.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2, or (at your option)
// any later version.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software Foundation,
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */
// Usage: printf format [argument...]
//
// A front end to the printf function that lets it be used from the shell.
//
// Backslash escapes:
//
// \" = double quote
// \\ = backslash
// \a = alert (bell)
// \b = backspace
// \c = produce no further output
// \e = escape
// \f = form feed
// \n = new line
// \r = carriage return
// \t = horizontal tab
// \v = vertical tab
// \ooo = octal number (ooo is 1 to 3 digits)
// \xhh = hexadecimal number (hhh is 1 to 2 digits)
// \uhhhh = 16-bit Unicode character (hhhh is 4 digits)
// \Uhhhhhhhh = 32-bit Unicode character (hhhhhhhh is 8 digits)
//
// Additional directive:
//
// %b = print an argument string, interpreting backslash escapes,
// except that octal escapes are of the form \0 or \0ooo.
//
// The `format' argument is re-used as many times as necessary
// to convert all of the given arguments.
//
// David MacKenzie <djm@gnu.ai.mit.edu>
// This file has been imported from source code of printf command in GNU Coreutils version 6.9.
use num_traits;
use super::prelude::*;
use crate::locale::{get_numeric_locale, Locale};
use crate::wchar::encode_byte_to_char;
use crate::wutil::{
errors::Error,
wcstod::wcstod,
wcstoi::{wcstoi_partial, Options as WcstoiOpts},
wstr_offset_in,
};
use printf_compat::args::ToArg;
use printf_compat::printf::sprintf_locale;
/// \return true if \p c is an octal digit.
fn is_octal_digit(c: char) -> bool {
('0'..='7').contains(&c)
}
/// \return true if \p c is a decimal digit.
fn iswdigit(c: char) -> bool {
c.is_ascii_digit()
}
/// \return true if \p c is a hexadecimal digit.
fn iswxdigit(c: char) -> bool {
c.is_ascii_hexdigit()
}
struct builtin_printf_state_t<'a> {
// Out and err streams. Note this is a captured reference!
streams: &'a mut io_streams_t,
// The status of the operation.
exit_code: c_int,
// Whether we should stop outputting. This gets set in the case of an error, and also with the
// \c escape.
early_exit: bool,
// Our output buffer, so we don't write() constantly.
// Our strategy is simple:
// We print once per argument, and we flush the buffer before the error.
buff: WString,
// The locale, which affects printf output and also parsing of floats due to decimal separators.
locale: Locale,
}
/// Convert to a scalar type. \return the result of conversion, and the end of the converted string.
/// On conversion failure, \p end is not modified.
trait RawStringToScalarType: Copy + num_traits::Zero + std::convert::From<u32> {
/// Convert from a string to our self type.
/// \return the result of conversion, and the remainder of the string.
fn raw_string_to_scalar_type<'a>(
s: &'a wstr,
locale: &Locale,
end: &mut &'a wstr,
) -> Result<Self, Error>;
/// Convert from a Unicode code point to this type.
/// This supports printf's ability to convert from char to scalar via a leading quote.
/// Try it:
/// > printf "%f" "'a"
/// 97.000000
/// Wild stuff.
fn from_ord(c: char) -> Self {
let as_u32: u32 = c.into();
as_u32.into()
}
}
impl RawStringToScalarType for i64 {
fn raw_string_to_scalar_type<'a>(
s: &'a wstr,
_locale: &Locale,
end: &mut &'a wstr,
) -> Result<Self, Error> {
let mut consumed = 0;
let res = wcstoi_partial(s, WcstoiOpts::default(), &mut consumed);
*end = s.slice_from(consumed);
res
}
}
impl RawStringToScalarType for u64 {
fn raw_string_to_scalar_type<'a>(
s: &'a wstr,
_locale: &Locale,
end: &mut &'a wstr,
) -> Result<Self, Error> {
let mut consumed = 0;
let res = wcstoi_partial(
s,
WcstoiOpts {
wrap_negatives: true,
..Default::default()
},
&mut consumed,
);
*end = s.slice_from(consumed);
res
}
}
impl RawStringToScalarType for f64 {
fn raw_string_to_scalar_type<'a>(
s: &'a wstr,
locale: &Locale,
end: &mut &'a wstr,
) -> Result<Self, Error> {
let mut consumed: usize = 0;
let mut result = wcstod(s, locale.decimal_point, &mut consumed);
if result.is_ok() && consumed == s.chars().count() {
*end = s.slice_from(consumed);
return result;
}
// The conversion using the user's locale failed. That may be due to the string not being a
// valid floating point value. It could also be due to the locale using different separator
// characters than the normal english convention. So try again by forcing the use of a locale
// that employs the english convention for writing floating point numbers.
consumed = 0;
result = wcstod(s, '.', &mut consumed);
if result.is_ok() {
*end = s.slice_from(consumed);
}
return result;
}
}
/// Convert a string to a scalar type.
/// Use state.verify_numeric to report any errors.
fn string_to_scalar_type<T: RawStringToScalarType>(
s: &wstr,
state: &mut builtin_printf_state_t,
) -> T {
if s.char_at(0) == '"' || s.char_at(0) == '\'' {
// Note that if the string is really just a leading quote,
// we really do want to convert the "trailing nul".
T::from_ord(s.char_at(1))
} else {
let mut end = s;
let mval = T::raw_string_to_scalar_type(s, &state.locale, &mut end);
state.verify_numeric(s, end, mval.err());
mval.unwrap_or(T::zero())
}
}
/// For each character in str, set the corresponding boolean in the array to the given flag.
fn modify_allowed_format_specifiers(ok: &mut [bool; 256], str: &str, flag: bool) {
for c in str.chars() {
ok[c as usize] = flag;
}
}
impl<'a> builtin_printf_state_t<'a> {
#[allow(clippy::partialeq_to_none)]
fn verify_numeric(&mut self, s: &wstr, end: &wstr, errcode: Option<Error>) {
// This check matches the historic `errcode != EINVAL` check from C++.
// Note that empty or missing values will be silently treated as 0.
if errcode != None && errcode != Some(Error::InvalidChar) && errcode != Some(Error::Empty) {
match errcode.unwrap() {
Error::Overflow => {
self.fatal_error(sprintf!("%ls: %ls", s, wgettext!("Number out of range")));
}
Error::Empty => {
self.fatal_error(sprintf!("%ls: %ls", s, wgettext!("Number was empty")));
}
Error::InvalidChar => {
panic!("Unreachable");
}
}
} else if !end.is_empty() {
if s.as_ptr() == end.as_ptr() {
self.fatal_error(wgettext_fmt!("%ls: expected a numeric value", s));
} else {
// This isn't entirely fatal - the value should still be printed.
self.nonfatal_error(wgettext_fmt!(
"%ls: value not completely converted (can't convert '%ls')",
s,
end
));
// Warn about octal numbers as they can be confusing.
// Do it if the unconverted digit is a valid hex digit,
// because it could also be an "0x" -> "0" typo.
if s.char_at(0) == '0' && iswxdigit(end.char_at(0)) {
self.nonfatal_error(wgettext_fmt!(
"Hint: a leading '0' without an 'x' indicates an octal number"
));
}
}
}
}
/// Evaluate a printf conversion specification. SPEC is the start of the directive, and CONVERSION
/// specifies the type of conversion. SPEC does not include any length modifier or the
/// conversion specifier itself. FIELD_WIDTH and PRECISION are the field width and
/// precision for '*' values, if HAVE_FIELD_WIDTH and HAVE_PRECISION are true, respectively.
/// ARGUMENT is the argument to be formatted.
#[allow(clippy::collapsible_else_if, clippy::too_many_arguments)]
fn print_direc(
&mut self,
spec: &wstr,
conversion: char,
have_field_width: bool,
field_width: i32,
have_precision: bool,
precision: i32,
argument: &wstr,
) {
/// Printf macro helper which provides our locale.
macro_rules! append_output_fmt {
(
$fmt:expr, // format string of type &wstr
$($arg:expr),* // arguments
) => {
// Don't output if we're done.
if !self.early_exit {
sprintf_locale(
&mut self.buff,
$fmt,
&self.locale,
&[$($arg.to_arg()),*]
)
}
}
}
// Start with everything except the conversion specifier.
let mut fmt = spec.to_owned();
// Create a copy of the % directive, with a width modifier substituted for any
// existing integer length modifier.
match conversion {
'x' | 'X' | 'd' | 'i' | 'o' | 'u' => {
fmt.push_str("ll");
}
'a' | 'e' | 'f' | 'g' | 'A' | 'E' | 'F' | 'G' => {
fmt.push_str("L");
}
's' | 'c' => {
fmt.push_str("l");
}
_ => {}
}
// Append the conversion itself.
fmt.push(conversion);
// Rebind as a ref.
let fmt: &wstr = &fmt;
match conversion {
'd' | 'i' => {
let arg: i64 = string_to_scalar_type(argument, self);
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, arg);
} else {
append_output_fmt!(fmt, precision, arg);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, arg);
} else {
append_output_fmt!(fmt, field_width, precision, arg);
}
}
}
'o' | 'u' | 'x' | 'X' => {
let arg: u64 = string_to_scalar_type(argument, self);
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, arg);
} else {
append_output_fmt!(fmt, precision, arg);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, arg);
} else {
append_output_fmt!(fmt, field_width, precision, arg);
}
}
}
'a' | 'A' | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' => {
let arg: f64 = string_to_scalar_type(argument, self);
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, arg);
} else {
append_output_fmt!(fmt, precision, arg);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, arg);
} else {
append_output_fmt!(fmt, field_width, precision, arg);
}
}
}
'c' => {
if !have_field_width {
append_output_fmt!(fmt, argument.char_at(0));
} else {
append_output_fmt!(fmt, field_width, argument.char_at(0));
}
}
's' => {
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, argument);
} else {
append_output_fmt!(fmt, precision, argument);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, argument);
} else {
append_output_fmt!(fmt, field_width, precision, argument);
}
}
}
_ => {
panic!("unexpected opt: {}", conversion);
}
}
}
/// Print the text in FORMAT, using ARGV for arguments to any `%' directives.
/// Return the number of elements of ARGV used.
fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
let mut argc = argv.len();
let save_argc = argc; /* Preserve original value. */
let mut f: &wstr; /* Pointer into `format'. */
let mut direc_start: &wstr; /* Start of % directive. */
let mut direc_length: usize; /* Length of % directive. */
let mut have_field_width: bool; /* True if FIELD_WIDTH is valid. */
let mut field_width: c_int = 0; /* Arg to first '*'. */
let mut have_precision: bool; /* True if PRECISION is valid. */
let mut precision = 0; /* Arg to second '*'. */
let mut ok = [false; 256]; /* ok['x'] is true if %x is allowed. */
// N.B. this was originally written as a loop like so:
// for (f = format; *f != L'\0'; ++f) {
// so we emulate that.
f = format;
let mut first = true;
loop {
if !first {
f = &f[1..];
}
first = false;
if f.is_empty() {
break;
}
match f.char_at(0) {
'%' => {
direc_start = f;
f = &f[1..];
direc_length = 1;
have_field_width = false;
have_precision = false;
if f.char_at(0) == '%' {
self.append_output('%');
continue;
}
if f.char_at(0) == 'b' {
// FIXME: Field width and precision are not supported for %b, even though POSIX
// requires it.
if argc > 0 {
self.print_esc_string(argv[0]);
argv = &argv[1..];
argc -= 1;
}
continue;
}
modify_allowed_format_specifiers(&mut ok, "aAcdeEfFgGiosuxX", true);
let mut continue_looking_for_flags = true;
while continue_looking_for_flags {
match f.char_at(0) {
'I' | '\'' => {
modify_allowed_format_specifiers(&mut ok, "aAceEosxX", false);
}
'-' | '+' | ' ' => {
// pass
}
'#' => {
modify_allowed_format_specifiers(&mut ok, "cdisu", false);
}
'0' => {
modify_allowed_format_specifiers(&mut ok, "cs", false);
}
_ => {
continue_looking_for_flags = false;
}
}
if continue_looking_for_flags {
f = &f[1..];
direc_length += 1;
}
}
if f.char_at(0) == '*' {
f = &f[1..];
direc_length += 1;
if argc > 0 {
let width: i64 = string_to_scalar_type(argv[0], self);
if (c_int::MIN as i64) <= width && width <= (c_int::MAX as i64) {
field_width = width as c_int;
} else {
self.fatal_error(wgettext_fmt!(
"invalid field width: %ls",
argv[0]
));
}
argv = &argv[1..];
argc -= 1;
} else {
field_width = 0;
}
have_field_width = true;
} else {
while iswdigit(f.char_at(0)) {
f = &f[1..];
direc_length += 1;
}
}
if f.char_at(0) == '.' {
f = &f[1..];
direc_length += 1;
modify_allowed_format_specifiers(&mut ok, "c", false);
if f.char_at(0) == '*' {
f = &f[1..];
direc_length += 1;
if argc > 0 {
let prec: i64 = string_to_scalar_type(argv[0], self);
if prec < 0 {
// A negative precision is taken as if the precision were omitted,
// so -1 is safe here even if prec < INT_MIN.
precision = -1;
} else if (c_int::MAX as i64) < prec {
self.fatal_error(wgettext_fmt!(
"invalid precision: %ls",
argv[0]
));
} else {
precision = prec as c_int;
}
argv = &argv[1..];
argc -= 1;
} else {
precision = 0;
}
have_precision = true;
} else {
while iswdigit(f.char_at(0)) {
f = &f[1..];
direc_length += 1;
}
}
}
while matches!(f.char_at(0), 'l' | 'L' | 'h' | 'j' | 't' | 'z') {
f = &f[1..];
}
let conversion = f.char_at(0);
if (conversion as usize) > 0xFF || !ok[conversion as usize] {
self.fatal_error(wgettext_fmt!(
"%.*ls: invalid conversion specification",
wstr_offset_in(f, direc_start) + 1,
direc_start
));
return 0;
}
let mut argument = L!("");
if argc > 0 {
argument = argv[0];
argv = &argv[1..];
argc -= 1;
}
self.print_direc(
&direc_start[..direc_length],
f.char_at(0),
have_field_width,
field_width,
have_precision,
precision,
argument,
);
}
'\\' => {
let consumed_minus_1 = self.print_esc(f, false);
f = &f[consumed_minus_1..]; // Loop increment will add 1.
}
c => {
self.append_output(c);
}
}
}
save_argc - argc
}
fn nonfatal_error<Str: AsRef<wstr>>(&mut self, errstr: Str) {
let errstr = errstr.as_ref();
// Don't error twice.
if self.early_exit {
return;
}
// If we have output, write it so it appears first.
if !self.buff.is_empty() {
self.streams.out.append(&self.buff);
self.buff.clear();
}
self.streams.err.append(errstr);
if !errstr.ends_with('\n') {
self.streams.err.append1('\n');
}
// We set the exit code to error, because one occurred,
// but we don't do an early exit so we still print what we can.
self.exit_code = STATUS_CMD_ERROR.unwrap();
}
fn fatal_error<Str: AsRef<wstr>>(&mut self, errstr: Str) {
let errstr = errstr.as_ref();
// Don't error twice.
if self.early_exit {
return;
}
// If we have output, write it so it appears first.
if !self.buff.is_empty() {
self.streams.out.append(&self.buff);
self.buff.clear();
}
self.streams.err.append(errstr);
if !errstr.ends_with('\n') {
self.streams.err.append1('\n');
}
self.exit_code = STATUS_CMD_ERROR.unwrap();
self.early_exit = true;
}
/// Print a \ escape sequence starting at ESCSTART.
/// Return the number of characters in the string, *besides the backslash*.
/// That is this is ONE LESS than the number of characters consumed.
/// If octal_0 is nonzero, octal escapes are of the form \0ooo, where o
/// is an octal digit; otherwise they are of the form \ooo.
fn print_esc(&mut self, escstart: &wstr, octal_0: bool) -> usize {
assert!(escstart.char_at(0) == '\\');
let mut p = &escstart[1..];
let mut esc_value = 0; /* Value of \nnn escape. */
let mut esc_length; /* Length of \nnn escape. */
if p.char_at(0) == 'x' {
// A hexadecimal \xhh escape sequence must have 1 or 2 hex. digits.
p = &p[1..];
esc_length = 0;
while esc_length < 2 && iswxdigit(p.char_at(0)) {
esc_value = esc_value * 16 + p.char_at(0).to_digit(16).unwrap();
esc_length += 1;
p = &p[1..];
}
if esc_length == 0 {
self.fatal_error(wgettext!("missing hexadecimal number in escape"));
}
self.append_output(encode_byte_to_char((esc_value % 256) as u8));
} else if is_octal_digit(p.char_at(0)) {
// Parse \0ooo (if octal_0 && *p == L'0') or \ooo (otherwise). Allow \ooo if octal_0 && *p
// != L'0'; this is an undocumented extension to POSIX that is compatible with Bash 2.05b.
// Wrap mod 256, which matches historic behavior.
esc_length = 0;
if octal_0 && p.char_at(0) == '0' {
p = &p[1..];
}
while esc_length < 3 && is_octal_digit(p.char_at(0)) {
esc_value = esc_value * 8 + p.char_at(0).to_digit(8).unwrap();
esc_length += 1;
p = &p[1..];
}
self.append_output(encode_byte_to_char((esc_value % 256) as u8));
} else if "\"\\abcefnrtv".contains(p.char_at(0)) {
self.print_esc_char(p.char_at(0));
p = &p[1..];
} else if p.char_at(0) == 'u' || p.char_at(0) == 'U' {
let esc_char: char = p.char_at(0);
p = &p[1..];
let mut uni_value = 0;
let exp_esc_length = if esc_char == 'u' { 4 } else { 8 };
for esc_length in 0..exp_esc_length {
if !iswxdigit(p.char_at(0)) {
// Escape sequence must be done. Complain if we didn't get anything.
if esc_length == 0 {
self.fatal_error(wgettext!("Missing hexadecimal number in Unicode escape"));
}
break;
}
uni_value = uni_value * 16 + p.char_at(0).to_digit(16).unwrap();
p = &p[1..];
}
// N.B. we assume __STDC_ISO_10646__.
if uni_value > 0x10FFFF {
self.fatal_error(wgettext_fmt!(
"Unicode character out of range: \\%c%0*x",
esc_char,
exp_esc_length,
uni_value
));
} else {
// TODO-RUST: if uni_value is a surrogate, we need to encode it using our PUA scheme.
if let Some(c) = char::from_u32(uni_value) {
self.append_output(c);
} else {
self.fatal_error(wgettext!("Invalid code points not yet supported by printf"));
}
}
} else {
self.append_output('\\');
if !p.is_empty() {
self.append_output(p.char_at(0));
p = &p[1..];
}
}
return wstr_offset_in(p, escstart) - 1;
}
/// Print string str, evaluating \ escapes.
fn print_esc_string(&mut self, mut str: &wstr) {
// Emulating the following loop: for (; *str; str++)
while !str.is_empty() {
let c = str.char_at(0);
if c == '\\' {
let consumed_minus_1 = self.print_esc(str, false);
str = &str[consumed_minus_1..];
} else {
self.append_output(c);
}
str = &str[1..];
}
}
/// Output a single-character \ escape.
fn print_esc_char(&mut self, c: char) {
match c {
'a' => {
// alert
self.append_output('\x07'); // \a
}
'b' => {
// backspace
self.append_output('\x08'); // \b
}
'c' => {
// cancel the rest of the output
self.early_exit = true;
}
'e' => {
// escape
self.append_output('\x1B');
}
'f' => {
// form feed
self.append_output('\x0C'); // \f
}
'n' => {
// new line
self.append_output('\n');
}
'r' => {
// carriage return
self.append_output('\r');
}
't' => {
// horizontal tab
self.append_output('\t');
}
'v' => {
// vertical tab
self.append_output('\x0B'); // \v
}
_ => {
self.append_output(c);
}
}
}
fn append_output(&mut self, c: char) {
// Don't output if we're done.
if self.early_exit {
return;
}
self.buff.push(c);
}
}
/// The printf builtin.
pub fn printf(
_parser: &mut parser_t,
streams: &mut io_streams_t,
argv: &mut [&wstr],
) -> Option<c_int> {
let mut argc = argv.len();
// Rebind argv as immutable slice (can't rearrange its elements), skipping the command name.
let mut argv: &[&wstr] = &argv[1..];
argc -= 1;
if argc < 1 {
return STATUS_INVALID_ARGS;
}
let mut state = builtin_printf_state_t {
streams,
exit_code: STATUS_CMD_OK.unwrap(),
early_exit: false,
buff: WString::new(),
locale: get_numeric_locale(),
};
let format = argv[0];
argc -= 1;
argv = &argv[1..];
loop {
let args_used = state.print_formatted(format, argv);
argc -= args_used;
argv = &argv[args_used..];
if !state.buff.is_empty() {
state.streams.out.append(&state.buff);
state.buff.clear();
}
if !(args_used > 0 && argc > 0 && !state.early_exit) {
break;
}
}
return Some(state.exit_code);
}

View File

@@ -0,0 +1,67 @@
//! Implementation of the pwd builtin.
use errno::errno;
use super::prelude::*;
use crate::{env::EnvMode, wutil::wrealpath};
// The pwd builtin. Respect -P to resolve symbolic links. Respect -L to not do that (the default).
const short_options: &wstr = L!("LPh");
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.appendln(pwd);
return STATUS_CMD_OK;
}

View File

@@ -0,0 +1,167 @@
use super::prelude::*;
use crate::wutil;
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.appendln(argv[i + 1 + rand]);
return STATUS_CMD_OK;
}
fn parse_ll(streams: &mut io_streams_t, cmd: &wstr, num: &wstr) -> Result<i64, wutil::Error> {
let res = fish_wcstol(num);
if res.is_err() {
streams
.err
.append(wgettext_fmt!("%ls: %ls: invalid integer\n", cmd, num));
}
return res;
}
fn parse_ull(streams: &mut io_streams_t, cmd: &wstr, num: &wstr) -> Result<u64, wutil::Error> {
let res = fish_wcstoul(num);
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_ll(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_ll(streams, cmd, argv[i]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => start = x,
}
match parse_ll(streams, cmd, argv[i + 1]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => end = x,
}
}
3 => {
// start, step, end
match parse_ll(streams, cmd, argv[i]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => start = x,
}
// start, step, end
match parse_ull(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_ll(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.appendln(result.to_wstring());
return STATUS_CMD_OK;
}

View File

@@ -0,0 +1,134 @@
//! Implementation of the realpath builtin.
use errno::errno;
use super::prelude::*;
use crate::{
path::path_apply_working_directory,
wutil::{normalize_path, wrealpath},
};
#[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().as_wstr());
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
}

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