Compare commits

..

2 Commits

Author SHA1 Message Date
Johannes Altmanninger
42063c7bbe builtin read: --tokenize-raw option
Closes #11084
2025-06-26 01:17:22 +02:00
Johannes Altmanninger
f8743c2b20 builtin commandline: --tokens-raw to include all tokens
Part of #11084
2025-06-26 01:17:22 +02:00
78 changed files with 810 additions and 5040 deletions

View File

@@ -1,22 +0,0 @@
name: Oldest Supported Rust Toolchain
on:
workflow_call:
inputs:
targets:
description: Comma-separated list of target triples to install for this toolchain
required: false
components:
description: Comma-separated list of components to be additionally installed
required: false
permissions:
contents: read
runs:
using: "composite"
steps:
- uses: dtolnay/rust-toolchain@1.70
with:
targets: ${{ inputs.targets }}
components: ${{ inputs.components}}

View File

@@ -1,22 +0,0 @@
name: Stable Rust Toolchain
on:
workflow_call:
inputs:
targets:
description: Comma-separated list of target triples to install for this toolchain
required: false
components:
description: Comma-separated list of components to be additionally installed
required: false
permissions:
contents: read
runs:
using: "composite"
steps:
- uses: dtolnay/rust-toolchain@1.88
with:
targets: ${{ inputs.targets }}
components: ${{ inputs.components }}

View File

@@ -9,12 +9,12 @@ jobs:
environment: macos-codesign
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: ./.github/actions/rust-toolchain@oldest-supported
- name: Install Rust 1.73.0
uses: dtolnay/rust-toolchain@1.73.0
with:
targets: x86_64-apple-darwin
- name: Install Rust Stable
uses: ./.github/actions/rust-toolchain@stable
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: build-and-codesign

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/rust-toolchain@oldest-supported
- uses: dtolnay/rust-toolchain@1.70
- name: Install deps
run: |
sudo apt install gettext libpcre2-dev python3-pexpect python3-sphinx tmux
@@ -47,7 +47,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/rust-toolchain@oldest-supported
- uses: dtolnay/rust-toolchain@1.70
with:
targets: "i586-unknown-linux-gnu" # rust-toolchain wants this comma-separated
- name: Install deps
@@ -128,7 +128,7 @@ jobs:
#
# steps:
# - uses: actions/checkout@v4
# - uses: ./.github/actions/rust-toolchain@oldest-supported
# - uses: dtolnay/rust-toolchain@1.70
# - name: Install deps
# run: |
# sudo apt install gettext libpcre2-dev python3-pexpect tmux
@@ -156,7 +156,7 @@ jobs:
CARGO_NET_GIT_FETCH_WITH_CLI: true
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/rust-toolchain@oldest-supported
- uses: dtolnay/rust-toolchain@1.70
- name: Install deps
run: |
# --break-system-packages because homebrew has now declared itself "externally managed".

View File

@@ -8,48 +8,47 @@ permissions:
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/rust-toolchain@stable
with:
components: rustfmt
- uses: dtolnay/rust-toolchain@stable
- name: cargo fmt
run: cargo fmt --check
clippy-stable:
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/rust-toolchain@stable
with:
components: clippy
- name: Install deps
run: |
sudo apt install gettext
- name: cargo clippy
run: cargo clippy --workspace --all-targets -- --deny=warnings
clippy-msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/rust-toolchain@oldest-supported
with:
components: clippy
- uses: dtolnay/rust-toolchain@stable
- name: Install deps
run: |
sudo apt install gettext
sudo apt install gettext libpcre2-dev
- name: cmake
run: |
cmake -B build
- name: cargo clippy
run: cargo clippy --workspace --all-targets -- --deny=warnings
# This used to have --deny=warnings, but that turns rust release day
# into automatic CI failure day, so we don't do that.
run: cargo clippy --workspace --all-targets
rustdoc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@stable
- name: cargo doc
run: |
RUSTDOCFLAGS='-D warnings' cargo doc --workspace
- name: cargo doctest
run: |
cargo test --doc --workspace
# Disabling for now because it also checks "advisories",
# making CI fail for reasons unrelated to the patch
# cargo-deny:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# - uses: EmbarkStudios/cargo-deny-action@v1

View File

@@ -16,7 +16,7 @@ jobs:
contents: read
steps:
- uses: ./.github/actions/rust-toolchain@oldest-supported
- uses: dtolnay/rust-toolchain@1.70
- uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
contents: read
steps:
- uses: ./.github/actions/rust-toolchain@oldest-supported
- uses: dtolnay/rust-toolchain@1.70
- uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -64,7 +64,6 @@ New or improved bindings
this is only enabled by default if the terminal advertises support for the ``indn`` capability via XTGETTCAP.
- Bindings using shift with non-ASCII letters (such as :kbd:`ctrl-shift-ä`) are now supported.
If there is any modifier other than shift, this is the recommended notation (as opposed to :kbd:`ctrl-Ä`).
- Vi mode has learned :kbd:`ctrl-a` (increment) and :kbd:`ctrl-x` (decrement) (:issue:`11570`).
Completions
^^^^^^^^^^^

View File

@@ -325,7 +325,7 @@ fn get_version(src_dir: &Path) -> String {
let path = src_dir.join("version");
if let Ok(strver) = read_to_string(path) {
return strver;
return strver.to_string();
}
let args = &["describe", "--always", "--dirty=-dirty"];
@@ -448,7 +448,10 @@ fn build_man(build_dir: &Path) {
);
}
Ok(out) => {
if !out.success() {
if out.success() {
// Success!
return;
} else {
panic!("sphinx-build failed to build the man pages.");
}
}

View File

@@ -30,7 +30,7 @@ X86_64_DEPLOY_TARGET='MACOSX_DEPLOYMENT_TARGET=10.9'
# The first supported version of macOS on arm64 is 10.15, so any Rust is fine for arm64.
# We wish to support back to 10.9 on x86-64; the last version of Rust to support that is
# version 1.73.0.
RUST_VERSION_X86_64=1.70.0
RUST_VERSION_X86_64=1.73.0
while getopts "sf:i:p:e:nj:" opt; do
case $opt in

View File

@@ -62,16 +62,17 @@ if set -q fish_files[1]
if not type -q fish_indent
echo
echo $yellow'Could not find `fish_indent` in `$PATH`.'$normal
exit 127
end
echo === Running "$green"fish_indent"$normal"
if set -l -q _flag_check
if not fish_indent --check -- $fish_files
echo $red"Fish files are not formatted correctly."$normal
exit 1
end
echo
else
fish_indent -w -- $fish_files
echo === Running "$green"fish_indent"$normal"
if set -l -q _flag_check
if not fish_indent --check -- $fish_files
echo $red"Fish files are not formatted correctly."$normal
exit 1
end
else
fish_indent -w -- $fish_files
end
end
end
@@ -79,16 +80,17 @@ if set -q python_files[1]
if not type -q black
echo
echo $yellow'Please install `black` to style python'$normal
exit 127
end
echo === Running "$green"black"$normal"
if set -l -q _flag_check
if not black --check $python_files
echo $red"Python files are not formatted correctly."$normal
exit 1
end
echo
else
black $python_files
echo === Running "$green"black"$normal"
if set -l -q _flag_check
if not black --check $python_files
echo $red"Python files are not formatted correctly."$normal
exit 1
end
else
black $python_files
end
end
end
@@ -96,29 +98,30 @@ if not cargo fmt --version >/dev/null
echo
echo $yellow'Please install "rustfmt" to style Rust, e.g. via:'
echo "rustup component add rustfmt"$normal
exit 127
end
echo === Running "$green"rustfmt"$normal"
if set -l -q _flag_check
if set -l -q _flag_all
if not cargo fmt --check
echo $red"Rust files are not formatted correctly."$normal
exit 1
echo
else
echo === Running "$green"rustfmt"$normal"
if set -l -q _flag_check
if set -l -q _flag_all
if not cargo fmt --check
echo $red"Rust files are not formatted correctly."$normal
exit 1
end
else
if set -q rust_files[1]
if not rustfmt --check --files-with-diff $rust_files
echo $red"Rust files are not formatted correctly."
exit 1
end
end
end
else
if set -q rust_files[1]
if not rustfmt --check --files-with-diff $rust_files
echo $red"Rust files are not formatted correctly."
exit 1
if set -l -q _flag_all
cargo fmt
else
if set -q rust_files[1]
rustfmt $rust_files
end
end
end
else
if set -l -q _flag_all
cargo fmt
else
if set -q rust_files[1]
rustfmt $rust_files
end
end
end

View File

@@ -83,7 +83,14 @@ The following options control how much is read and how it is stored:
**-n** or **--nchars** *NCHARS*
Makes ``read`` return after reading *NCHARS* characters or the end of the line, whichever comes first.
**-t** -or **--tokenize**
**-t** -or **--tokenize** or **--tokenize-raw**
Causes read to split the input into variables by the shell's tokenization rules.
This means it will honor quotes and escaping.
This option is of course incompatible with other options to control splitting like **--delimiter** and does not honor :envvar:`IFS` (like fish's tokenizer).
The **-t** -or **--tokenize** variants perform quote removal, so e.g. ``a\ b`` is stored as ``a b``.
However variables and command substitutions are not expanded.
**--tokenize-raw**
Causes read to split the input into variables by the shell's tokenization rules. This means it will honor quotes and escaping. This option is of course incompatible with other options to control splitting like **--delimiter** and does not honor :envvar:`IFS` (like fish's tokenizer). It saves the tokens in the manner they'd be passed to commands on the commandline, so e.g. ``a\ b`` is stored as ``a b``. Note that currently it leaves command substitutions intact along with the parentheses.
**-a** or **--list**

View File

@@ -507,13 +507,7 @@ Command mode is also known as normal mode.
- :kbd:`backspace` moves the cursor left.
- :kbd:`g,g` / :kbd:`G` moves the cursor to the beginning/end of the commandline, respectively.
- :kbd:`~` toggles the case (upper/lower) of the character and moves to the next character.
- :kbd:`g,u` lowercases to the end of the word.
- :kbd:`g,U` uppercases to the end of the word.
- :kbd:`g` / :kbd:`G` moves the cursor to the beginning/end of the commandline, respectively.
- :kbd:`:,q` exits fish.
@@ -557,10 +551,6 @@ Visual mode
- :kbd:`~` toggles the case (upper/lower) on the selection, and enters :ref:`command mode <vi-mode-command>`.
- :kbd:`g,u` lowercases the selection, and enters :ref:`command mode <vi-mode-command>`.
- :kbd:`g,U` uppercases the selection, and enters :ref:`command mode <vi-mode-command>`.
- :kbd:`",*,y` copies the selection to the clipboard, and enters :ref:`command mode <vi-mode-command>`.
.. _custom-binds:

View File

@@ -6,9 +6,11 @@ ENV LC_ALL=C.UTF-8
RUN zypper --non-interactive install \
bash \
cmake \
diffutils \
gcc-c++ \
git-core \
ninja \
pcre2-devel \
python311 \
python311-pip \
@@ -33,6 +35,4 @@ WORKDIR /home/fishuser
COPY fish_run_tests.sh /
ENV FISH_CHECK_LINT=false
CMD /fish_run_tests.sh

471
po/de.po

File diff suppressed because it is too large Load Diff

471
po/en.po

File diff suppressed because it is too large Load Diff

471
po/fr.po

File diff suppressed because it is too large Load Diff

471
po/pl.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

471
po/sv.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -279,7 +279,6 @@ fn format_a(mut y: f64, params: FormatParams<'_, impl Write>) -> Result<usize, E
// Compute the number of hex digits in the mantissa after the decimal.
// -1 for leading 1 bit (we are to the range [1, 2)), then divide by 4, rounding up.
#[allow(unknown_lints)] // for old clippy
#[allow(clippy::manual_div_ceil)]
const MANTISSA_HEX_DIGITS: usize = (MANTISSA_BITS - 1 + 3) / 4;
if had_prec && prec < MANTISSA_HEX_DIGITS {
@@ -496,7 +495,7 @@ fn format_mantissa_e(
let digit = if d < decimal.len_i32() { decimal[d] } else { 0 };
let min_width = if d > 0 { DIGIT_WIDTH } else { 1 };
buf.clear();
write!(buf, "{digit:0min_width$}")?;
write!(buf, "{:0width$}", digit, width = min_width)?;
let mut s = buf.as_str();
if d == 0 {
// First digit. Emit it, and likely also a decimal point.

View File

@@ -464,7 +464,7 @@ pub fn sprintf_locale(
let uint = arg.as_uint()?;
if uint != 0 {
prefix = "0x";
write!(buf, "{uint:x}")?;
write!(buf, "{:x}", uint)?;
}
buf
}
@@ -478,9 +478,9 @@ pub fn sprintf_locale(
prefix = if lower { "0x" } else { "0X" };
}
if lower {
write!(buf, "{uint:x}")?;
write!(buf, "{:x}", uint)?;
} else {
write!(buf, "{uint:X}")?;
write!(buf, "{:X}", uint)?;
}
}
buf
@@ -488,7 +488,7 @@ pub fn sprintf_locale(
CS::o => {
let uint = arg.as_uint()?;
if uint != 0 {
write!(buf, "{uint:o}")?;
write!(buf, "{:o}", uint)?;
}
if flags.alt_form && desired_precision.unwrap_or(0) <= buf.len() + 1 {
desired_precision = Some(buf.len() + 1);
@@ -498,7 +498,7 @@ pub fn sprintf_locale(
CS::u => {
let uint = arg.as_uint()?;
if uint != 0 {
write!(buf, "{uint}")?;
write!(buf, "{}", uint)?;
}
buf
}

View File

@@ -890,7 +890,7 @@ fn test_exhaustive(rust_fmt: &str, c_fmt: *const c_char) {
// "There's only 4 billion floats so test them all."
// This tests a format string expected to be of the form "%.*g" or "%.*e".
// That is, it takes a precision and a double.
println!("Testing {rust_fmt}");
println!("Testing {}", rust_fmt);
let mut rust_str = String::with_capacity(128);
let mut c_storage = [0u8; 128];
let c_storage_ptr = c_storage.as_mut_ptr() as *mut c_char;

View File

@@ -1 +0,0 @@
cilium completion fish 2>/dev/null | source

View File

@@ -1,102 +0,0 @@
# cjpm.fish - Fish completion script for Cangjie Package Manager
# Global options
complete -c cjpm -l help -s h -d "Help for cjpm"
complete -c cjpm -l version -s v -d "Version for cjpm"
# Subcommands
complete -c cjpm -n __fish_use_subcommand -f -a init -d "Init a new cangjie module"
complete -c cjpm -n __fish_use_subcommand -f -a check -d "Check the dependencies"
complete -c cjpm -n __fish_use_subcommand -f -a update -d "Update cjpm.lock"
complete -c cjpm -n __fish_use_subcommand -f -a tree -d "Display the package dependencies in the source code"
complete -c cjpm -n __fish_use_subcommand -f -a build -d "Compile the current module"
complete -c cjpm -n __fish_use_subcommand -f -a run -d "Compile and run an executable product"
complete -c cjpm -n __fish_use_subcommand -f -a test -d "Unittest a local package or module"
complete -c cjpm -n __fish_use_subcommand -f -a bench -d "Run benchmarks in a local package or module"
complete -c cjpm -n __fish_use_subcommand -f -a clean -d "Clean up the target directory"
complete -c cjpm -n __fish_use_subcommand -f -a install -d "Install a cangjie binary"
complete -c cjpm -n __fish_use_subcommand -f -a uninstall -d "Uninstall a cangjie binary"
# 'init' subcommand options
complete -c cjpm -n "__fish_seen_subcommand_from init" -f -l help -s h -d "Help for init"
complete -c cjpm -n "__fish_seen_subcommand_from init" -f -l workspace -d "Initialize a workspace's default configuration file"
complete -c cjpm -n "__fish_seen_subcommand_from init" -f -l name -d "Specify root package name (default: current directory)" -r
complete -c cjpm -n "__fish_seen_subcommand_from init" -l path -d "Specify path to create the module (default: current directory)" -r
complete -c cjpm -n "__fish_seen_subcommand_from init" -f -l type -d "Define output type of current module" -r -f -a "executable static dynamic"
# 'run' subcommand options
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -l name -d "Name of the executable product to run (default: main)" -r
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -l build-args -d "Arguments to pass to the build process" -r
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -l skip-build -d "Skip compile, only run the executable product"
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -l run-args -d "Arguments to pass to the executable product" -r
complete -c cjpm -n "__fish_seen_subcommand_from run" -l target-dir -d "Specify target directory" -r
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -s g -d "Enable debug version"
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -s h -l help -d "Help for run"
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -s V -l verbose -d "Enable verbose"
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -l skip-script -d "Disable script 'build.cj'"
# 'install' subcommand options
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -s h -l help -d "Help for install"
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -s V -l verbose -d "Enable verbose"
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -s m -l member -d "Specify a member module of the workspace" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -s g -d "Enable install debug version target"
complete -c cjpm -n "__fish_seen_subcommand_from install" -l path -d "Specify path of source module (default: current path)" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -l root -d "Specify path of installed binary" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l git -d "Specify URL of installed git module" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l branch -d "Specify branch of installed git module" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l tag -d "Specify tag of installed git module" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l commit -d "Specify commit ID of installed git module" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -s j -l jobs -d "Number of jobs to spawn in parallel" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l cfg -d "Enable the customized option 'cfg'"
complete -c cjpm -n "__fish_seen_subcommand_from install" -l target-dir -d "Specify target directory" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l name -d "Specify product name to install (default: all)" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l skip-build -d "Install binary in target directory without building"
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l list -d "List all installed modules and their versions"
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l skip-script -d "Disable script 'build.cj'"
# 'build' subcommand options
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s h -l help -d "Help for build"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s i -l incremental -d "Enable incremental compilation"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s j -l jobs -d "Number of jobs to spawn in parallel" -r
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s V -l verbose -d "Enable verbose"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s g -d "Enable compile debug version target"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -l coverage -d "Enable coverage"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -l cfg -d "Enable the customized option 'cfg'"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s m -l member -d "Specify a member module of the workspace" -r
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -l target -d "Generate code for the given target platform" -r
complete -c cjpm -n "__fish_seen_subcommand_from build" -l target-dir -d "Specify target directory" -r
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s o -l output -d "Specify product name when compiling an executable file" -r
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s l -l lint -d "Enable cjlint code check"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -l mock -d "Enable support of mocking classes in tests"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -l skip-script -d "Disable script 'build.cj'"
# 'test' subcommand options
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s h -l help -d "Help for test"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s j -l jobs -d "Number of jobs to spawn in parallel" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s V -l verbose -d "Enable verbose"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s g -d "Enable compile debug version tests"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s i -l incremental -d "Enable incremental compilation"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l no-run -d "Compile, but don't run tests"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l skip-build -d "Skip compile, only run tests"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l coverage -d "Enable coverage"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l cfg -d "Enable the customized option 'cfg'"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l module -d "Specify modules to test (default: current module)" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s m -l member -d "Specify a member module of the workspace" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l target -d "Unittest for the given target platform" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -l target-dir -d "Specify target directory" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l dry-run -d "Print tests without execution"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l filter -d "Enable filter test" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l include-tags -d "Run tests with specified tags" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l exclude-tags -d "Run tests without specified tags" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l no-color -d "Enable colorless result output"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l random-seed -d "Enable random seed" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l timeout-each -d "Specify default timeout for test cases" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l parallel -d "Number of workers running tests" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l show-all-output -d "Show output for all test cases"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l no-capture-output -d "Disable test output capturing"
complete -c cjpm -n "__fish_seen_subcommand_from test" -l report-path -d "Specify path to directory of report" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l report-format -d "Specify format of report" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l skip-script -d "Disable script 'build.cj'"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l no-progress -d "Disable progress report"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l progress-brief -d "Display brief progress report"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l progress-entries-limit -d "Limit number of entries shown in progress report"

View File

@@ -1,45 +0,0 @@
# fish completion for Perl's cpan
function __fish_cpan_list_installed_modules
# Following IRC's #fish suggestion to use </dev/null as cpan might go interactive
cpan -l </dev/null | while read -l line
# Filter out unrelated messages or notifications
if string match -qr -- '^\w.*\t\w.*$' $line
string replace -r -- '\t.*' '' $line |
string escape --style=script
end
end
end
complete -c cpan -s a -d "Creates a CPAN.pm autobundle with CPAN::Shell->autobundle"
complete -c cpan -s A -d "Show primary maintainer for specified module" -xa "(__fish_cpan_list_installed_modules)"
complete -c cpan -s c -d "Runs a `make clean` in the specified modules directories"
complete -c cpan -s C -d "Show the Changes files for specified module" -a "(__fish_cpan_list_installed_modules)"
complete -c cpan -s D -d "Show module details" -a "(__fish_cpan_list_installed_modules)"
complete -c cpan -s f -d "Force the specified action"
complete -c cpan -s F -d "Turn off CPAN.pm's attempts to lock anything"
complete -c cpan -s g -d "Download latest distribution of module to current directory" -a "(__fish_cpan_list_installed_modules)"
# complete -c cpan -s G -d "UNIMPLEMENTED"
complete -c cpan -s h -d "Print a help message and exit"
complete -c cpan -s i -d "Install specified module"
complete -c cpan -s I -d "Load 'local::lib' (think like '-I' for loading lib paths)"
complete -c cpan -s j -d "Load file with CPAN configuration data"
complete -c cpan -s J -d "Dump the configuration in the same format that CPAN.pm uses"
complete -c cpan -s l -d "list all installed modules with their versions"
complete -c cpan -s L -d "List the modules by the specified authors"
complete -c cpan -s m -d "Make the specified modules"
complete -c cpan -s M -d "Comma-separated list of mirrors to use for this run" -x
#complete -c cpan -s n -d "Do a dry run, but dont actually install anything. (unimplemented)"
complete -c cpan -s O -d "Show the out-of-date modules"
complete -c cpan -s p -d "Ping the configured mirrors and print a report"
complete -c cpan -s P -d "Find and the best mirrors available"
complete -c cpan -s r -d "Recompiles dynamically loaded modules with CPAN::Shell->recompile"
complete -c cpan -s s -d "Drop in the CPAN.pm shell"
complete -c cpan -s t -d "Run a `make test` on the specified modules"
complete -c cpan -s T -d "Do not test modules. Simply install them"
complete -c cpan -s u -d "Upgrade all installed modules"
complete -c cpan -s v -d "Print the script version and CPAN.pm version then exit"
complete -c cpan -s V -d "Print detailed information about the cpan client"
# complete -c cpan -s w -d "UNIMPLEMENTED"
complete -c cpan -s x -d "Find close matches to named module. Requires Text::Levenshtein or others"
complete -c cpan -s X -d "Dump all the namespaces to standard output"

View File

@@ -1,6 +1,5 @@
complete -c fish_indent -s h -l help -d 'Display help and exit'
complete -c fish_indent -s v -l version -d 'Display version and exit'
complete -c fish_indent -s c -l check -d 'Do not indent, only return 0 if the code is already indented as fish_indent would'
complete -c fish_indent -s i -l no-indent -d 'Do not indent output, only reformat into one job per line'
complete -c fish_indent -l only-indent -d 'Do not reformat, only indent lines'
complete -c fish_indent -l only-unindent -d 'Do not reformat, only unindent lines'

View File

@@ -2023,9 +2023,8 @@ __fish_git_add_revision_completion -n '__fish_git_using_command rebase'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l continue -d 'Restart the rebasing process'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l abort -d 'Abort the rebase operation'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l edit-todo -d 'Edit the todo list'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l skip -d 'Restart the rebasing process by skipping the current patch'
complete -f -c git -n '__fish_git_using_command rebase' -l keep-empty -d "Keep the commits that don't change anything"
complete -f -c git -n '__fish_git_using_command rebase' -l keep-base -d 'Keep the base commit as-is'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l skip -d 'Restart the rebasing process by skipping the current patch'
complete -f -c git -n '__fish_git_using_command rebase' -s m -l merge -d 'Use merging strategies to rebase'
complete -f -c git -n '__fish_git_using_command rebase' -s q -l quiet -d 'Be quiet'
complete -f -c git -n '__fish_git_using_command rebase' -s v -l verbose -d 'Be verbose'

View File

@@ -1 +0,0 @@
hubble completion fish 2>/dev/null | source

View File

@@ -1 +0,0 @@
k9s completion fish 2>/dev/null | source

View File

@@ -24,7 +24,6 @@ complete -c objdump -l target -s b -d "Specify target object format" -x -a "elf6
complete -c objdump -l architecture -s m -d "Specify target architecture" -x -a "i386 i386:x86-64 i386:x64-32 i8086 i386:intel i386:x86-64:intel i386:x64-32:intel i386:nacl i386:x86-64:nacl i386:x64-32:nacl iamcu iamcu:intel l1om l1om:intel k1om k1om:intel plugin"
complete -c objdump -l section -s j -d "Only display information for given section" -x
complete -c objdump -l disassembler-options -s M -d "Pass given options on to disassembler" -x
complete -c objdump -l disassembler-color -d "Control disassembler syntax highlighting style" -x -a "off terminal on extended"
complete -c objdump -l endian -x -d "Set format endianness when disassembling" -a "big little"
complete -c objdump -o EB -d "Assume big endian format when disassembling"
complete -c objdump -o EL -d "Assume little endian format when disassembling"

View File

@@ -2,16 +2,11 @@ function __fish_ollama_list
ollama list 2>/dev/null | tail -n +2 | string replace --regex "\s.*" ""
end
function __fish_ollama_ps
ollama ps 2>/dev/null | tail -n +2 | string replace --regex "\s.*" ""
end
complete -f -c ollama
complete -c ollama -n __fish_use_subcommand -a serve -d "Start ollama"
complete -c ollama -n __fish_use_subcommand -a create -d "Create a model from a Modelfile"
complete -c ollama -n __fish_use_subcommand -a show -d "Show information for a model"
complete -c ollama -n __fish_use_subcommand -a run -d "Run a model"
complete -c ollama -n __fish_use_subcommand -a stop -d "Stop a running model."
complete -c ollama -n __fish_use_subcommand -a pull -d "Pull a model from a registry"
complete -c ollama -n __fish_use_subcommand -a push -d "Push a model to a registry"
complete -c ollama -n __fish_use_subcommand -a list -d "List models"
@@ -24,4 +19,3 @@ complete -c ollama -f -a "(__fish_ollama_list)" --condition '__fish_seen_subcomm
complete -c ollama -f -a "(__fish_ollama_list)" --condition '__fish_seen_subcommand_from run'
complete -c ollama -f -a "(__fish_ollama_list)" --condition '__fish_seen_subcommand_from cp'
complete -c ollama -f -a "(__fish_ollama_list)" --condition '__fish_seen_subcommand_from rm'
complete -c ollama -f -a "(__fish_ollama_ps)" --condition '__fish_seen_subcommand_from stop'

View File

@@ -1,9 +0,0 @@
complete -c protontricks-launch -f -s h -l help -d 'Show help message and exit'
complete -c protontricks-launch -l no-term -d 'Specify no terminal is available for errors, use dialogs instead'
complete -c protontricks-launch -s v -l verbose -d 'Increase log verbosity, can be supplied twice'
complete -c protontricks-launch -l no-runtime -d 'Disable Steam Runtime'
complete -c protontricks-launch -l no-bwrap -d 'Disable bwrap containerization when using Steam Runtime'
complete -c protontricks-launch -l background-wineserver -d 'Launch a wineserver process to improve Wine startup time'
complete -c protontricks-launch -l no-background-wineserver -d 'Do not launch wineserver process'
complete -c protontricks-launch -l appid -xka '(__fish_protontricks_complete_appid)'
complete -c protontricks-launch -l cwd-app -d 'Change to the Steam app directory when launching command'

View File

@@ -1,24 +0,0 @@
function __fish_protontricks_complete_winetricks_command
complete -C 'winetricks '
end
function __fish_protontricks_is_search
__fish_contains_opt -s s search
end
complete -c protontricks -f
complete -c protontricks -n 'not __fish_protontricks_is_search' -n '__fish_is_nth_token 1' -ka '(__fish_protontricks_complete_appid)'
complete -c protontricks -n 'not __fish_protontricks_is_search' -n 'not __fish_is_nth_token 1' -a '(__fish_protontricks_complete_winetricks_command)'
complete -c protontricks -s h -l help -d 'Show help message and exit'
complete -c protontricks -s v -l verbose -d 'Increase log verbosity, can be supplied twice'
complete -c protontricks -l no-term -d 'Specify that no terminal is available to Protontricks'
complete -c protontricks -s s -l search -d 'Search for game(s) with the given name'
complete -c protontricks -s l -l list -d 'List all apps'
complete -c protontricks -s c -l command -xa '(__fish_complete_subcommand)' -d 'Run a command with Wine-related environment variables set'
complete -c protontricks -l gui -d 'Launch the Protontricks GUI'
complete -c protontricks -l no-runtime -d 'Disable Steam Runtime'
complete -c protontricks -l no-bwrap -d 'Disable bwrap containerization when using Steam Runtime'
complete -c protontricks -l background-wineserver -d 'Launch a wineserver process to improve Wine startup time'
complete -c protontricks -l no-background-wineserver -d 'Do not launch wineserver process'
complete -c protontricks -l cwd-app -d 'Change to the Steam app directory when launching command'
complete -c protontricks -s V -l version -d 'Show version number and exit'

View File

@@ -3,8 +3,8 @@ set -l commands list-units list-sockets start stop reload restart try-restart re
isolate kill is-active is-failed status show get-cgroup-attr set-cgroup-attr unset-cgroup-attr set-cgroup help \
reset-failed list-unit-files enable disable is-enabled reenable preset mask unmask link load list-jobs cancel dump \
list-dependencies snapshot delete daemon-reload daemon-reexec show-environment set-environment unset-environment \
default rescue emergency halt poweroff reboot kexec exit suspend suspend-then-hibernate hibernate hybrid-sleep switch-root \
list-timers set-property import-environment get-default list-automounts is-system-running try-reload-or-restart freeze \
default rescue emergency halt poweroff reboot kexec exit suspend hibernate hybrid-sleep switch-root list-timers \
set-property import-environment get-default list-automounts is-system-running try-reload-or-restart freeze \
thaw mount-image bind clean
if test $systemd_version -gt 208 2>/dev/null
set commands $commands cat

View File

@@ -1,31 +0,0 @@
# fish completion for t-rec (https://github.com/sassman/t-rec-rs
function __fish_t_rec_time_unit
set -l cur (commandline --current-token)
if string match -qr '^\d+$' -- $cur
echo $cur"ms"\t"milliseconds"
echo $cur"s"\t"seconds"
echo $cur"m"\t"minutes"
end
end
function __fish_t_rec_window_list
string replace -r -- '\s*(.*)\|\s+(\d+)' '$2\t$1' (t-rec --ls | tail -n +2)
end
# Options
complete -c t-rec -d "Command to run instead of shell" -xa "(complete -C '' | string split \t -f1)"
complete -c t-rec -s v -l verbose -d "Enable verbose insights for the curious"
complete -c t-rec -s q -l quiet -d "Quiet mode, suppresses the banner"
complete -c t-rec -s m -l video -d "Generate both gif and mp4 video"
complete -c t-rec -s M -l video-only -d "Generate only mp4 video, not gif"
complete -c t-rec -s d -l decor -d "Decorate animation" -xa "shadow none"
complete -c t-rec -s b -l bg -d "Background color when decors are used" -xa "white black transparent"
complete -c t-rec -s n -l natural -d "Natural typing, disables idle detection and sampling optimization"
complete -c t-rec -s l -l ls -d "List windows available for recording by their id"
complete -c t-rec -s w -l win-id -d "Id of window to capture" -xa "(__t_rec_window_list)"
complete -c t-rec -s e -l end-pause -d "Pause time at end of animation" -xa "(__fish_t_rec_time_unit)" -r
complete -c t-rec -s s -l start-pause -d "Pause time at start of animation" -xa "(__fish_t_rec_time_unit)" -r
complete -c t-rec -s o -l output -d "Output file (without extension); defaults to t-rec" -r
complete -c t-rec -s h -l help -d "Print help"
complete -c t-rec -s V -l version -d "Print version"

View File

@@ -1,52 +0,0 @@
function __fish_tmuxp_ls
tmuxp ls 2>/dev/null
end
complete -c tmuxp -f
complete -c tmuxp -s h -l help -d Help
complete -c tmuxp -s V -l version -d Version
complete -c tmuxp -l log-level -x -a "debug info warning error critical" -d "Log level"
complete -c tmuxp -n __fish_use_subcommand -a load -d "Load tmuxp workspace"
complete -c tmuxp -n __fish_use_subcommand -a shell -d "Launch python shell"
complete -c tmuxp -n __fish_use_subcommand -a import -d "Import workspace"
complete -c tmuxp -n __fish_use_subcommand -a convert -d "Convert workspace"
complete -c tmuxp -n __fish_use_subcommand -a debug-info -d "Print diagnostics"
complete -c tmuxp -n __fish_use_subcommand -a ls -d "List workspaces"
complete -c tmuxp -n __fish_use_subcommand -a edit -d "Edit workspace"
complete -c tmuxp -n __fish_use_subcommand -a freeze -d "Freeze session to worskpace"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -a "(__fish_tmuxp_ls)"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s L -r -F -d "Passthru to tmux -L"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s S -r -F -d "Passthru to tmux -S"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s f -r -F -d "Passthru to tmux -f"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s s -r -d "Session name"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s d -d "Detached session"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s a -r -d "Attach current windows to session"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s y -l yes -d "Always answer yes"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s 2 -d "Assume 256 color support"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s 8 -d "Assume 88 color support"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -l log-file -r -F -d "Log file path"
complete -c tmuxp -n "__fish_seen_subcommand_from import" -a "teamocil tmuxinator"
complete -c tmuxp -n "__fish_seen_subcommand_from convert" -s y -l yes -d "Always answer yes"
complete -c tmuxp -n "__fish_seen_subcommand_from convert" -r -F
complete -c tmuxp -n "__fish_seen_subcommand_from edit" -a "(__fish_tmuxp_ls)"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s L -r -F -d "Passthru to tmux -L"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s S -r -F -d "Passthru to tmux -S"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s f -l worskpace-format -x -a "json yaml" -d "File format"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s o -l save-to -r -F -d "Output file"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s y -l yes -d "Always answer yes"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s q -l quiet -d "Do not prompt for confirmation"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -l force -d "Overwrite existing file"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -s L -r -F -d "Passthru to tmux -L"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -s S -r -F -d "Passthru to tmux -S"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -s c -r -d "Run code and exit"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l best -d "Use best shell available"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l pdb -d "Use pdb"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l code -d "Use code.interact()"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l ptipython -d "Use ptpython+ipython"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l ptpython -d "Use ptpython"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l ipython -d "Use ipython"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l bpython -d "Use bpython"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l use-pythonrc -d "Load ~/.pythonrc.py"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l no-startup -d "Do not load ~/.pythonrc.py"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l use-vi-mode -d "Use vi input mode"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l no-vi-mode -d "Do not use vi input mode"

View File

@@ -1 +0,0 @@
volta completions fish 2>/dev/null | source

View File

@@ -76,11 +76,6 @@ function __fish_print_hostnames -d "Print a list of known hostnames"
set -l new_paths
for path in $paths
# while ssh_config is using brackets to resolve env, they should be removed
# example
# in ssh_config: ${SOME_PATH}
# in fish: $SOME_PATH
set path (string replace -r '\${([^}]+)}' '$1' $path)
set -l expanded_path
# Scope "relative" paths in accordance to ssh path resolution
if string match -qrv '^[~/]' $path

View File

@@ -1,5 +0,0 @@
function __fish_protontricks_complete_appid
protontricks -l |
string match --regex '.*\(\d+\)' |
string replace --regex '(.*) \((\d+)\)' '$2\t$1'
end

View File

@@ -52,13 +52,11 @@ function fish_default_key_bindings -d "emacs-like key binds"
bind --preset $argv alt-c capitalize-word
if test (__fish_uname) = Darwin
bind --preset $argv alt-backspace backward-kill-word
bind --preset $argv ctrl-alt-h backward-kill-word
bind --preset $argv ctrl-backspace backward-kill-token
bind --preset $argv alt-delete kill-word
bind --preset $argv ctrl-delete kill-token
else
bind --preset $argv alt-backspace backward-kill-token
bind --preset $argv ctrl-alt-h backward-kill-token
bind --preset $argv ctrl-backspace backward-kill-word
bind --preset $argv alt-delete kill-token
bind --preset $argv ctrl-delete kill-word

View File

@@ -1,80 +1,3 @@
alias fish_vi_dec 'fish_vi_inc_dec dec'
alias fish_vi_inc 'fish_vi_inc_dec inc'
# TODO: Currently we do not support hexadecimal and octal values.
function fish_vi_inc_dec --description 'increment or decrement the number below the cursor'
# The cursor is zero based, but all string functions assume 1 to be
# the lowest index. Adjust accordingly.
set --local cursor (math -- (commandline --cursor) + 1)
set --local line (commandline --current-buffer | string collect)
set --local candidate (string sub --start $cursor -- $line | string collect)
if set --local just_found (string match --regex '^-?[0-9]+' -- $candidate)
# Search from the current cursor position backwards for as long as we
# can identify a valid number.
set --function found $just_found
set --function found_at $cursor
set --local end (math -- $cursor + (string length -- $found) - 1)
set i (math -- $cursor - 1)
while [ $i -ge 1 ]
set candidate (string sub --start $i --end $end -- $line)
if set just_found (string match --regex '^-?[0-9]+$' -- $candidate)
set found $just_found
set found_at $i
# We found a candidate, but continue to make sure that we captured
# the complete number and not just part of it.
else
# We have already found a number earlier. Work with that.
break
end
set i (math -- $i - 1)
end
else
# We didn't find a match below the cursor. Mirror Vim behavior by
# checking ahead as well.
for i in (seq (math -- $cursor + 1) (math -- (string length -- $line) - 1))
set candidate (string sub --start $i -- $line | string collect)
if set just_found (string match --regex '^-?[0-9]+' -- $candidate)
set found $just_found
set found_at $i
break
end
end
if [ -z "$found" ]
return
end
end
if [ $argv = inc ]
set number (math -- $found + 1)
else if [ $argv = dec ]
set number (math -- $found - 1)
end
set --local number_abs (string trim --left --chars=- -- $number)
set --local signed $status
set --local found_abs (string trim --left --chars=- -- $found)
set number (string pad --char 0 --width (string length -- $found_abs) -- $number_abs)
if test $signed -eq 0
set number "-$number"
end
# `string sub` may bitch about `--end` being zero if `found_at` is 1.
# So ignore errors here...
set --local before (string sub --end (math -- $found_at - 1) -- $line 2> /dev/null | string collect)
set --local after (string sub --start (math -- $found_at + (string length -- $found)) -- $line | string collect)
commandline --replace -- "$before$number$after"
# Need to subtract two here because 1) cursor is zero based 2)
# `found_at` is the index of the first character of the match, but we
# want the one before that.
commandline --cursor -- (math -- $found_at + (string length -- $number) - 2)
commandline --function -- repaint
end
function fish_vi_key_bindings --description 'vi-like key bindings for fish'
if contains -- -h $argv
or contains -- --help $argv
@@ -348,12 +271,6 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
bind -s --preset -M replace backspace backward-char
bind -s --preset -M replace shift-backspace backward-char
#
# Increment or decrement number under the cursor with ctrl+x ctrl+a
#
bind -s --preset -M default ctrl-a fish_vi_inc
bind -s --preset -M default ctrl-x fish_vi_dec
#
# visual mode
#
@@ -400,8 +317,7 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
bind -s --preset -M visual -m default '",*,y' "fish_clipboard_copy; commandline -f end-selection repaint-mode"
bind -s --preset -M visual -m default '",+,y' "fish_clipboard_copy; commandline -f end-selection repaint-mode"
bind -s --preset -M visual -m default '~' togglecase-selection end-selection repaint-mode
bind -s --preset -M visual -m default g,u downcase-selection end-selection repaint-mode
bind -s --preset -M visual -m default g,U upcase-selection end-selection repaint-mode
bind -s --preset -M visual -m default g,U togglecase-selection end-selection repaint-mode
bind -s --preset -M visual -m default ctrl-c end-selection repaint-mode
bind -s --preset -M visual -m default escape end-selection repaint-mode

View File

@@ -47,7 +47,7 @@ function funced --description 'Edit function definition'
functions --no-details -- $funcname | __fish_indent --only-unindent | __fish_indent --no-indent | read -z init
end
set -l prompt 'printf "%s%s%s> " (set_color green) $funcname (set_color normal)'
set -l prompt 'printf "%s%s%s> " (set_color green) '$funcname' (set_color normal)'
if read -p $prompt -c "$init" --shell cmd
echo -n $cmd | __fish_indent --only-unindent | read -lz cmd
eval "$cmd"

View File

@@ -315,7 +315,7 @@ def unparse_color(col):
if col["bold"]:
ret += " --bold"
if col["underline"] is not None:
ret += " --underline=" + str(col["underline"])
ret += " --underline=" + col["underline"]
if col["italics"]:
ret += " --italics"
if col["dim"]:

View File

@@ -250,8 +250,6 @@ pub fn is_same_node(lhs: &dyn Node, rhs: &dyn Node) -> bool {
}
// Same base pointer and same vtable => same object.
#[allow(renamed_and_removed_lints)]
#[allow(clippy::vtable_address_comparisons)] // for old clippy
if std::ptr::eq(lhs, rhs) {
return true;
}

View File

@@ -166,7 +166,7 @@ fn source_config_in_directory(parser: &Parser, dir: &wstr) -> bool {
parser.libdata_mut().within_fish_init = true;
let _ = parser.eval(&cmd, &IoChain::new());
parser.libdata_mut().within_fish_init = false;
true
return true;
}
/// Parse init files. exec_path is the path of fish executable as determined by argv[0].

View File

@@ -1,4 +1,5 @@
use super::prelude::*;
use super::read::TokenOutputMode;
use crate::ast::{self, Kind, Leaf};
use crate::common::{unescape_string, UnescapeFlags, UnescapeStringStyle};
use crate::complete::Completion;
@@ -44,12 +45,6 @@ enum AppendMode {
Append,
}
enum TokenOutputMode {
Expanded,
Raw,
Unescaped,
}
/// Replace/append/insert the selection with/at/after the specified string.
///
/// \param begin beginning of selection
@@ -223,13 +218,15 @@ fn write_part(
if cut_at_cursor && token.end() >= pos {
break;
}
let is_redirection_target = in_redirection;
in_redirection = token.type_ == TokenType::redirect;
if is_redirection_target && token.type_ == TokenType::string {
continue;
}
if token.type_ != TokenType::string {
continue;
if token_mode != TokenOutputMode::Raw {
let is_redirection_target = in_redirection;
in_redirection = token.type_ == TokenType::redirect;
if is_redirection_target && token.type_ == TokenType::string {
continue;
}
if token.type_ != TokenType::string {
continue;
}
}
let token_text = tok.text_of(&token);
@@ -636,16 +633,19 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
transient = parser.libdata().transient_commandline.clone().unwrap();
current_buffer = &transient;
current_cursor_pos = transient.len();
} else if is_interactive_session() {
} else if rstate.initialized {
current_buffer = &rstate.text;
current_cursor_pos = rstate.cursor_pos;
} else {
// There is no command line because we are not interactive.
streams.err.append(cmd);
streams
.err
.append(L!(": Can not set commandline in non-interactive mode\n"));
builtin_print_error_trailer(parser, streams.err, cmd);
// There is no command line, either because we are not interactive, or because we are
// interactive and are still reading init files (in which case we silently ignore this).
if !is_interactive_session() {
streams.err.append(cmd);
streams
.err
.append(L!(": Can not set commandline in non-interactive mode\n"));
builtin_print_error_trailer(parser, streams.err, cmd);
}
return Err(STATUS_CMD_ERROR);
}

View File

@@ -8,7 +8,6 @@
use crate::parse_constants::ParseErrorList;
use crate::parse_util::parse_util_detect_errors_in_argument_list;
use crate::parse_util::{parse_util_detect_errors, parse_util_token_extent};
use crate::proc::is_interactive_session;
use crate::reader::{commandline_get_state, completion_apply_to_command_line};
use crate::wcstringutil::string_suffixes_string;
use crate::{
@@ -466,12 +465,11 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
None => {
// No argument given, try to use the current commandline.
let commandline_state = commandline_get_state(true);
if !is_interactive_session() {
streams.err.append(cmd);
streams
.err
.append(L!(": Can not get commandline in non-interactive mode\n"));
return Err(STATUS_CMD_ERROR);
if !commandline_state.initialized {
// This corresponds to using 'complete -C' in non-interactive mode.
// See #2361 .
builtin_missing_argument(parser, streams, cmd, L!("-C"), true);
return Err(STATUS_INVALID_ARGS);
}
commandline_state.text
}

View File

@@ -110,7 +110,7 @@ fn parse_numeric_sequence<I>(chars: I) -> Option<(usize, u8)>
return None;
}
let mut val: u8 = 0;
let mut val = 0;
let mut consumed = start;
for digit in chars
.skip(start)
@@ -120,7 +120,7 @@ fn parse_numeric_sequence<I>(chars: I) -> Option<(usize, u8)>
// base is either 8 or 16, so digit can never be >255
let digit = u8::try_from(digit).unwrap();
val = val.wrapping_mul(base).wrapping_add(digit);
val = val * base + digit;
consumed += 1;
}

View File

@@ -2,10 +2,10 @@
use crate::fds::make_fd_blocking;
use crate::proc::Pid;
use crate::reader::{reader_save_screen_state, reader_write_title};
use crate::reader::reader_write_title;
use crate::tokenizer::tok_command;
use crate::wutil::perror;
use crate::{env::EnvMode, tty_handoff::TtyHandoff};
use crate::{env::EnvMode, proc::TtyTransfer};
use libc::{STDIN_FILENO, TCSADRAIN};
use super::prelude::*;
@@ -139,13 +139,12 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
// Note if tty transfer fails, we still try running the job.
parser.job_promote_at(job_pos);
let mut handoff = TtyHandoff::new(reader_save_screen_state);
let _ = make_fd_blocking(STDIN_FILENO);
{
let job_group = job.group();
job_group.set_is_foreground(true);
if job.entitled_to_terminal() {
handoff.disable_tty_protocols();
crate::input_common::terminal_protocols_disable_ifn();
}
let tmodes = job_group.tmodes.borrow();
if job_group.wants_terminal() && tmodes.is_some() {
@@ -156,15 +155,16 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
}
}
}
handoff.to_job_group(job.group.as_ref().unwrap());
let mut transfer = TtyTransfer::new();
transfer.to_job_group(job.group.as_ref().unwrap());
let resumed = job.resume();
if resumed {
job.continue_job(parser);
}
if job.is_stopped() {
handoff.save_tty_modes();
transfer.save_tty_modes();
}
handoff.reclaim();
transfer.reclaim();
if resumed {
Ok(SUCCESS)
} else {

View File

@@ -359,7 +359,6 @@ fn compute_multi_line_brace_statement_locations(&self) -> Vec<usize> {
{
next_newline += 1;
}
#[allow(clippy::nonminimal_bool)] // for old clippy; false positive?
let contains_newline = next_newline != newline_offsets.len() && {
let newline_offset = newline_offsets[next_newline];
assert!(newline_offset >= brace_statement.source_range().start());

View File

@@ -7,7 +7,7 @@
//!
//! Type "exit" or "quit" to terminate the program.
use std::{cell::RefCell, ops::ControlFlow, os::unix::prelude::OsStrExt};
use std::{cell::RefCell, ops::ControlFlow, os::unix::prelude::OsStrExt, sync::atomic::Ordering};
use libc::{STDIN_FILENO, TCSANOW, VEOF, VINTR};
use once_cell::unsync::OnceCell;
@@ -20,8 +20,8 @@
env::{env_init, EnvStack, Environment},
future_feature_flags,
input_common::{
match_key_event_to_key, CharEvent, InputEventQueue, InputEventQueuer, KeyEvent,
QueryResponseEvent, TerminalQuery,
match_key_event_to_key, terminal_protocol_hacks, terminal_protocols_enable_ifn, CharEvent,
InputEventQueue, InputEventQueuer, KeyEvent, QueryResponseEvent, TerminalQuery,
},
key::{char_to_symbol, Key},
nix::isatty,
@@ -30,13 +30,9 @@
proc::set_interactive_session,
reader::{check_exit_loop_maybe_warning, initial_query, reader_init},
signal::signal_set_handlers,
terminal::Capability,
terminal::{Capability, KITTY_KEYBOARD_SUPPORTED},
threads,
topic_monitor::topic_monitor_init,
tty_handoff::{
get_kitty_keyboard_capability, initialize_tty_metadata, set_kitty_keyboard_capability,
TtyHandoff,
},
wchar::prelude::*,
wgetopt::{wopt, ArgType, WGetopter, WOption},
};
@@ -91,16 +87,16 @@ fn process_input(streams: &mut IoStreams, continuous_mode: bool, verbose: bool)
let mut recent_chars = vec![];
streams.err.appendln("Press a key:\n");
let mut handoff = TtyHandoff::new(|| {});
handoff.enable_tty_protocols();
while (!first_char_seen || continuous_mode) && !check_exit_loop_maybe_warning(None) {
terminal_protocols_enable_ifn();
let kevt = match queue.readch() {
CharEvent::Key(kevt) => kevt,
CharEvent::Readline(_) | CharEvent::Command(_) | CharEvent::Implicit(_) => continue,
CharEvent::QueryResponse(QueryResponseEvent::PrimaryDeviceAttribute) => {
if get_kitty_keyboard_capability() == Capability::Unknown {
set_kitty_keyboard_capability(|| {}, Capability::NotSupported);
if KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed) == Capability::Unknown as _ {
KITTY_KEYBOARD_SUPPORTED
.store(Capability::NotSupported as _, Ordering::Release);
}
continue;
}
@@ -155,7 +151,7 @@ fn setup_and_process_keys(
// We need to set the shell-modes for ICRNL,
// in fish-proper this is done once a command is run.
unsafe { libc::tcsetattr(0, TCSANOW, &*shell_modes()) };
initialize_tty_metadata();
terminal_protocol_hacks();
let blocking_query: OnceCell<RefCell<Option<TerminalQuery>>> = OnceCell::new();
initial_query(&blocking_query, streams.out, None);

View File

@@ -12,17 +12,17 @@
use crate::env::READ_BYTE_LIMIT;
use crate::env::{EnvVar, EnvVarFlags};
use crate::input_common::decode_input_byte;
use crate::input_common::terminal_protocols_disable_ifn;
use crate::input_common::DecodeState;
use crate::input_common::InvalidPolicy;
use crate::nix::isatty;
use crate::reader::commandline_set_buffer;
use crate::reader::reader_save_screen_state;
use crate::reader::ReaderConfig;
use crate::reader::{reader_pop, reader_push, reader_readline};
use crate::tokenizer::Tok;
use crate::tokenizer::Tokenizer;
use crate::tokenizer::TOK_ACCEPT_UNFINISHED;
use crate::tokenizer::TOK_ARGUMENT_LIST;
use crate::tty_handoff::TtyHandoff;
use crate::wcstringutil::split_about;
use crate::wcstringutil::split_string_tok;
use crate::wutil;
@@ -33,6 +33,13 @@
use std::os::fd::RawFd;
use std::sync::atomic::Ordering;
#[derive(Clone, Copy, Eq, PartialEq)]
pub(crate) enum TokenOutputMode {
Expanded,
Raw,
Unescaped,
}
#[derive(Default)]
struct Options {
print_help: bool,
@@ -40,11 +47,11 @@ struct Options {
prompt: Option<WString>,
prompt_str: Option<WString>,
right_prompt: WString,
commandline: Option<WString>,
commandline: WString,
// If a delimiter was given. Used to distinguish between the default
// empty string and a given empty delimiter.
delimiter: Option<WString>,
tokenize: bool,
token_mode: Option<TokenOutputMode>, // never expanded
shell: bool,
array: bool,
silent: bool,
@@ -83,10 +90,19 @@ fn new() -> Self {
wopt(L!("shell"), ArgType::NoArgument, 'S'),
wopt(L!("silent"), ArgType::NoArgument, 's'),
wopt(L!("tokenize"), ArgType::NoArgument, 't'),
wopt(L!("tokenize-raw"), ArgType::NoArgument, '\x01'),
wopt(L!("unexport"), ArgType::NoArgument, 'u'),
wopt(L!("universal"), ArgType::NoArgument, 'U'),
];
fn tokenize_flag(token_mode: TokenOutputMode) -> &'static wstr {
match token_mode {
TokenOutputMode::Expanded => panic!(),
TokenOutputMode::Raw => L!("--tokenize-raw"),
TokenOutputMode::Unescaped => L!("--tokenize"),
}
}
fn parse_cmd_opts(
args: &mut [&wstr],
parser: &Parser,
@@ -101,7 +117,7 @@ fn parse_cmd_opts(
opts.array = true;
}
'c' => {
opts.commandline = Some(w.woptarg.unwrap().to_owned());
opts.commandline = w.woptarg.unwrap().to_owned();
}
'd' => {
opts.delimiter = Some(w.woptarg.unwrap().to_owned());
@@ -166,8 +182,28 @@ fn parse_cmd_opts(
'S' => {
opts.shell = true;
}
't' => {
opts.tokenize = true;
't' | '\x01' => {
let new_mode = match opt {
't' => TokenOutputMode::Unescaped,
'\x01' => TokenOutputMode::Raw,
_ => unreachable!(),
};
if let Some(old_mode) = opts.token_mode {
if old_mode != new_mode {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
wgettext_fmt!(
"%s and %s are mutually exclusive",
tokenize_flag(old_mode),
tokenize_flag(new_mode),
)
));
builtin_print_error_trailer(parser, streams.err, cmd);
return Err(STATUS_INVALID_ARGS);
}
}
opts.token_mode = Some(new_mode);
}
'U' => {
opts.place |= EnvMode::UNIVERSAL;
@@ -208,7 +244,7 @@ fn read_interactive(
silent: bool,
prompt: &wstr,
right_prompt: &wstr,
commandline: &Option<WString>,
commandline: &wstr,
inputfd: RawFd,
) -> BuiltinResult {
let mut exit_res = Ok(SUCCESS);
@@ -239,16 +275,13 @@ fn read_interactive(
s.readonly_commandline = false;
})
});
if let Some(commandline) = commandline {
commandline_set_buffer(parser, Some(commandline.clone()), None);
}
commandline_set_buffer(parser, Some(commandline.to_owned()), None);
let mline = {
let _interactive = parser.push_scope(|s| s.is_interactive = true);
let mut scoped_handoff = TtyHandoff::new(reader_save_screen_state);
scoped_handoff.enable_tty_protocols();
reader_readline(parser, NonZeroUsize::try_from(nchars).ok())
};
terminal_protocols_disable_ifn();
if let Some(line) = mline {
*buff = line;
if nchars > 0 && nchars < buff.len() {
@@ -486,24 +519,34 @@ fn validate_read_args(
return Err(STATUS_INVALID_ARGS);
}
if opts.tokenize && opts.delimiter.is_some() {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
"--delimiter",
"--tokenize"
));
return Err(STATUS_INVALID_ARGS);
fn tokenize_flag(token_mode: TokenOutputMode) -> &'static wstr {
match token_mode {
TokenOutputMode::Expanded => panic!(),
TokenOutputMode::Raw => L!("--tokenize-raw"),
TokenOutputMode::Unescaped => L!("--tokenize"),
}
}
if opts.tokenize && opts.one_line {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
"--line",
"--tokenize"
));
return Err(STATUS_INVALID_ARGS);
if let Some(token_mode) = opts.token_mode {
if opts.delimiter.is_some() {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
"--delimiter",
tokenize_flag(token_mode),
));
return Err(STATUS_INVALID_ARGS);
}
if opts.one_line {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
"--line",
tokenize_flag(token_mode),
));
return Err(STATUS_INVALID_ARGS);
}
}
// Verify all variable names.
@@ -636,18 +679,28 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
return exit_res;
}
if opts.tokenize {
if let Some(token_mode) = opts.token_mode {
let mut tok = Tokenizer::new(&buff, TOK_ACCEPT_UNFINISHED | TOK_ARGUMENT_LIST);
let token_text = |tokenizer: &mut Tokenizer<'_>, token: &Tok| -> WString {
let mut text = Cow::Borrowed(tokenizer.text_of(token));
match token_mode {
TokenOutputMode::Expanded => panic!(),
TokenOutputMode::Raw => (),
TokenOutputMode::Unescaped => {
if let Some(unescaped) =
unescape_string(&text, UnescapeStringStyle::default())
{
text = Cow::Owned(unescaped);
}
}
};
text.into_owned()
};
if opts.array {
// Array mode: assign each token as a separate element of the sole var.
let mut tokens = vec![];
while let Some(t) = tok.next() {
let text = tok.text_of(&t);
if let Some(out) = unescape_string(text, UnescapeStringStyle::default()) {
tokens.push(out);
} else {
tokens.push(text.to_owned());
}
tokens.push(token_text(&mut tok, &t));
}
parser.set_var_and_fire(argv[var_ptr], opts.place, tokens);
@@ -657,9 +710,7 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
let Some(t) = tok.next() else {
break;
};
let text = tok.text_of(&t);
let out = unescape_string(text, UnescapeStringStyle::default())
.unwrap_or_else(|| text.to_owned());
let out = token_text(&mut tok, &t);
parser.set_var_and_fire(argv[var_ptr], opts.place, vec![out]);
var_ptr += 1;
}

View File

@@ -1355,7 +1355,7 @@ pub fn valid_func_name(name: &wstr) -> bool {
/// A rusty port of the C++ `write_loop()` function from `common.cpp`. This should be deprecated in
/// favor of native rust read/write methods at some point.
pub fn safe_write_loop<Fd: AsRawFd>(fd: &Fd, buf: &[u8]) -> std::io::Result<()> {
pub fn write_loop<Fd: AsRawFd>(fd: &Fd, buf: &[u8]) -> std::io::Result<()> {
let fd = fd.as_raw_fd();
let mut total = 0;
while total < buf.len() {
@@ -1374,8 +1374,6 @@ pub fn safe_write_loop<Fd: AsRawFd>(fd: &Fd, buf: &[u8]) -> std::io::Result<()>
Ok(())
}
pub use safe_write_loop as write_loop;
// Output writes always succeed; this adapter allows us to use it in a write-like macro.
struct OutputWriteAdapter<'a, T: Output>(&'a mut T);

View File

@@ -117,7 +117,7 @@
FLOG!(config, "Using compiled in paths:");
paths = ConfigPaths {
data,
data: data.clone(),
sysconf: PathBuf::from(SYSCONF_DIR).join("fish"),
doc: DOC_DIR.into(),
bin,

View File

@@ -622,7 +622,6 @@ pub fn use_posix_spawn() -> bool {
}
/// Whether or not we are running on an OS where we allow ourselves to use `posix_spawn()`.
#[allow(clippy::needless_bool)] // for old clippy
const fn allow_use_posix_spawn() -> bool {
// OpenBSD's posix_spawn returns status 127 instead of erroring with ENOEXEC when faced with a
// shebang-less script. Disable posix_spawn on OpenBSD.

View File

@@ -38,12 +38,12 @@
use crate::proc::{
hup_jobs, is_interactive_session, jobs_requiring_warning_on_exit, no_exec,
print_exit_warning_for_jobs, InternalProc, Job, JobGroupRef, ProcStatus, Process, ProcessType,
TtyTransfer,
};
use crate::reader::{reader_run_count, safe_restore_term_mode};
use crate::redirection::{dup2_list_resolve_chain, Dup2List};
use crate::threads::{iothread_perform_cant_wait, is_forked_child};
use crate::trace::trace_if_enabled_with_args;
use crate::tty_handoff::TtyHandoff;
use crate::wchar::prelude::*;
use crate::wchar_ext::ToWString;
use crate::wutil::{fish_wcstol, perror};
@@ -110,7 +110,7 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
let deferred_process = get_deferred_process(job);
// We may want to transfer tty ownership to the pgroup leader.
let mut handoff = TtyHandoff::new(|| {});
let mut transfer = TtyTransfer::new();
// This loop loops over every process_t in the job, starting it as appropriate. This turns out
// to be rather complex, since a process_t can be one of many rather different things.
@@ -175,7 +175,7 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
// Transfer tty?
if p.leads_pgrp && job.group().wants_terminal() {
handoff.to_job_group(job.group.as_ref().unwrap());
transfer.to_job_group(job.group.as_ref().unwrap());
}
}
drop(pipe_next_read);
@@ -236,9 +236,9 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
}
if job.is_stopped() {
handoff.save_tty_modes();
transfer.save_tty_modes();
}
handoff.reclaim();
transfer.reclaim();
true
}

View File

@@ -38,6 +38,8 @@
pub struct ExpandFlags : u16 {
/// Fail expansion if there is a command substitution.
const FAIL_ON_CMDSUBST = 1 << 0;
/// Skip command substitutions.
const SKIP_CMDSUBST = 1 << 14;
/// Skip variable expansion.
const SKIP_VARIABLES = 1 << 1;
/// Skip wildcard expansion.
@@ -73,8 +75,6 @@ pub struct ExpandFlags : u16 {
const SPECIAL_FOR_COMMAND = 1 << 13;
/// The token has an unclosed brace, so don't add a space.
const NO_SPACE_FOR_UNCLOSED_BRACE = 1 << 14;
/// Skip command substitutions.
const SKIP_CMDSUBST = 1 << 15;
}
}

View File

@@ -766,7 +766,7 @@ fn test_ispath() {
let tester = temp.file_tester();
let file_path = temp.filepath("file.txt");
File::create(file_path).unwrap();
File::create(&file_path).unwrap();
let result = tester.test_path(L!("file.txt"), false);
assert!(result);
@@ -794,7 +794,7 @@ fn test_ispath() {
// Directories are also files.
let dir_path = temp.filepath("somedir");
create_dir_all(dir_path).unwrap();
create_dir_all(&dir_path).unwrap();
let result = tester.test_path(L!("somedir"), false);
assert!(result);
@@ -818,7 +818,7 @@ fn test_iscdpath() {
// rather than IsFile(false).
let dir_path = temp.filepath("somedir");
create_dir_all(dir_path).unwrap();
create_dir_all(&dir_path).unwrap();
let result = tester.test_cd_path(L!("somedir"), false);
assert_eq!(result, Ok(IsFile(true)));

View File

@@ -144,7 +144,6 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat
make_md(L!("delete-char"), ReadlineCmd::DeleteChar),
make_md(L!("delete-or-exit"), ReadlineCmd::DeleteOrExit),
make_md(L!("down-line"), ReadlineCmd::DownLine),
make_md(L!("downcase-selection"), ReadlineCmd::DowncaseSelection),
make_md(L!("downcase-word"), ReadlineCmd::DowncaseWord),
make_md(L!("end-of-buffer"), ReadlineCmd::EndOfBuffer),
make_md(L!("end-of-history"), ReadlineCmd::EndOfHistory),
@@ -206,7 +205,6 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat
make_md(L!("transpose-words"), ReadlineCmd::TransposeWords),
make_md(L!("undo"), ReadlineCmd::Undo),
make_md(L!("up-line"), ReadlineCmd::UpLine),
make_md(L!("upcase-selection"), ReadlineCmd::UpcaseSelection),
make_md(L!("upcase-word"), ReadlineCmd::UpcaseWord),
make_md(L!("yank"), ReadlineCmd::Yank),
make_md(L!("yank-pop"), ReadlineCmd::YankPop),

View File

@@ -1,18 +1,28 @@
use crate::common::{
fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes,
str2wcstring, WSL,
str2wcstring, ScopeGuard, WSL,
};
use crate::env::{EnvStack, Environment};
use crate::fd_readable_set::{FdReadableSet, Timeout};
use crate::flog::{FloggableDebug, FloggableDisplay, FLOG};
use crate::fork_exec::flog_safe::FLOG_SAFE;
use crate::global_safety::RelaxedAtomicBool;
use crate::key::{
self, alt, canonicalize_control_char, canonicalize_keyed_control_char, char_to_symbol, ctrl,
function_key, shift, Key, Modifiers, ViewportPosition,
};
use crate::reader::{reader_save_screen_state, reader_test_and_clear_interrupted};
use crate::terminal::{Capability, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE};
use crate::threads::iothread_port;
use crate::tty_handoff::{get_kitty_keyboard_capability, set_kitty_keyboard_capability};
use crate::reader::{reader_current_data, reader_test_and_clear_interrupted};
use crate::terminal::TerminalCommand::{
ApplicationKeypadModeDisable, ApplicationKeypadModeEnable, DecrstBracketedPaste,
DecrstFocusReporting, DecsetBracketedPaste, DecsetFocusReporting,
KittyKeyboardProgressiveEnhancementsDisable, KittyKeyboardProgressiveEnhancementsEnable,
ModifyOtherKeysDisable, ModifyOtherKeysEnable,
};
use crate::terminal::{
Capability, Output, Outputter, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED,
SCROLL_FORWARD_TERMINFO_CODE,
};
use crate::threads::{iothread_port, is_main_thread};
use crate::universal_notifier::default_notifier;
use crate::wchar::{encode_byte_to_char, prelude::*};
use crate::wutil::encoding::{mbrtowc, mbstate_t, zero_mbstate};
@@ -21,7 +31,9 @@
use std::collections::VecDeque;
use std::mem::MaybeUninit;
use std::os::fd::RawFd;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::os::unix::ffi::OsStrExt;
use std::ptr;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
// The range of key codes for inputrc-style keyboard functions.
@@ -96,8 +108,6 @@ pub enum ReadlineCmd {
DowncaseWord,
CapitalizeWord,
TogglecaseChar,
UpcaseSelection,
DowncaseSelection,
TogglecaseSelection,
Execute,
BeginningOfBuffer,
@@ -512,35 +522,8 @@ enum ReadbResult {
NothingToRead,
}
fn readb(in_fd: RawFd, blocking: bool, pasting: bool) -> ReadbResult {
let do_readb = || {
let mut arr: [u8; 1] = [0];
if read_blocked(in_fd, &mut arr) != Ok(1) {
// The terminal has been closed.
return ReadbResult::Eof;
}
let c = arr[0];
FLOG!(reader, "Read byte", char_to_symbol(char::from(c), true));
// The common path is to return a u8.
return ReadbResult::Byte(c);
};
fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult {
assert!(in_fd >= 0, "Invalid in fd");
if !blocking {
return if check_fd_readable(
in_fd,
Duration::from_millis(
if pasting || get_kitty_keyboard_capability() == Capability::Supported {
300
} else {
1
},
),
) {
do_readb()
} else {
ReadbResult::NothingToRead
};
}
let mut fdset = FdReadableSet::new();
loop {
fdset.clear();
@@ -558,7 +541,11 @@ fn readb(in_fd: RawFd, blocking: bool, pasting: bool) -> ReadbResult {
}
// Here's where we call select().
let select_res = fdset.check_readable(Timeout::Forever);
let select_res = fdset.check_readable(if blocking {
Timeout::Forever
} else {
Timeout::Duration(Duration::from_millis(1))
});
if select_res < 0 {
let err = errno::errno().0;
if err == libc::EINTR || err == libc::EAGAIN {
@@ -570,18 +557,32 @@ fn readb(in_fd: RawFd, blocking: bool, pasting: bool) -> ReadbResult {
}
}
// select() did not return an error, so we may have a readable fd.
// The priority order is: uvars, stdin, ioport.
// Check to see if we want a universal variable barrier.
if let Some(notifier_fd) = notifier_fd {
if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd) {
return ReadbResult::UvarNotified;
if blocking {
// select() did not return an error, so we may have a readable fd.
// The priority order is: uvars, stdin, ioport.
// Check to see if we want a universal variable barrier.
if let Some(notifier_fd) = notifier_fd {
if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd)
{
return ReadbResult::UvarNotified;
}
}
}
// Check stdin.
if fdset.test(in_fd) {
return do_readb();
let mut arr: [u8; 1] = [0];
if read_blocked(in_fd, &mut arr) != Ok(1) {
// The terminal has been closed.
return ReadbResult::Eof;
}
let c = arr[0];
FLOG!(reader, "Read byte", char_to_symbol(char::from(c), true));
// The common path is to return a u8.
return ReadbResult::Byte(c);
}
if !blocking {
return ReadbResult::NothingToRead;
}
// Check for iothread completions only if there is no data to be read from the stdin.
@@ -592,55 +593,6 @@ fn readb(in_fd: RawFd, blocking: bool, pasting: bool) -> ReadbResult {
}
}
pub fn check_fd_readable(in_fd: RawFd, timeout: Duration) -> bool {
use std::ptr;
// We are not prepared to handle a signal immediately; we only want to know if we get input on
// our fd before the timeout. Use pselect to block all signals; we will handle signals
// before the next call to readch().
let mut sigs = MaybeUninit::uninit();
let mut sigs = unsafe {
libc::sigfillset(sigs.as_mut_ptr());
sigs.assume_init()
};
// pselect expects timeouts in nanoseconds.
const NSEC_PER_MSEC: u64 = 1000 * 1000;
const NSEC_PER_SEC: u64 = NSEC_PER_MSEC * 1000;
let wait_nsec: u64 = (timeout.as_millis() as u64) * NSEC_PER_MSEC;
let timeout = libc::timespec {
tv_sec: (wait_nsec / NSEC_PER_SEC).try_into().unwrap(),
tv_nsec: (wait_nsec % NSEC_PER_SEC).try_into().unwrap(),
};
// We have one fd of interest.
let mut fdset = MaybeUninit::uninit();
let mut fdset = unsafe {
libc::FD_ZERO(fdset.as_mut_ptr());
fdset.assume_init()
};
unsafe {
libc::FD_SET(in_fd, &mut fdset);
}
let res = unsafe {
libc::pselect(
in_fd + 1,
&mut fdset,
ptr::null_mut(),
ptr::null_mut(),
&timeout,
&sigs,
)
};
// Prevent signal starvation on WSL causing the `torn_escapes.py` test to fail
if is_windows_subsystem_for_linux(WSL::V1) {
// Merely querying the current thread's sigmask is sufficient to deliver a pending signal
let _ = unsafe { libc::pthread_sigmask(0, ptr::null(), &mut sigs) };
}
res > 0
}
// Update the wait_on_escape_ms value in response to the fish_escape_delay_ms user variable being
// set.
pub fn update_wait_on_escape_ms(vars: &EnvStack) {
@@ -691,6 +643,120 @@ pub fn update_wait_on_sequence_key_ms(vars: &EnvStack) {
}
}
static TERMINAL_PROTOCOLS: AtomicBool = AtomicBool::new(false);
static BRACKETED_PASTE: AtomicBool = AtomicBool::new(false);
static IS_TMUX: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub(crate) static IN_MIDNIGHT_COMMANDER: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub(crate) static IN_DVTM: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
static ITERM_NO_KITTY_KEYBOARD: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub fn terminal_protocol_hacks() {
use std::env::var_os;
IN_MIDNIGHT_COMMANDER.store(var_os("MC_TMPDIR").is_some());
IN_DVTM
.store(var_os("TERM").is_some_and(|term| term.as_os_str().as_bytes() == b"dvtm-256color"));
IS_TMUX.store(var_os("TMUX").is_some());
ITERM_NO_KITTY_KEYBOARD.store(
var_os("LC_TERMINAL").is_some_and(|term| term.as_os_str().as_bytes() == b"iTerm2")
&& var_os("LC_TERMINAL_VERSION").is_some_and(|version| {
let Some(version) = parse_version(&str2wcstring(version.as_os_str().as_bytes()))
else {
return false;
};
version < (3, 5, 12)
}),
);
}
fn parse_version(version: &wstr) -> Option<(i64, i64, i64)> {
let mut numbers = version.split('.');
let major = fish_wcstol(numbers.next()?).ok()?;
let minor = fish_wcstol(numbers.next()?).ok()?;
let patch = numbers.next()?;
let patch = &patch[..patch
.chars()
.position(|c| !c.is_ascii_digit())
.unwrap_or(patch.len())];
let patch = fish_wcstol(patch).ok()?;
Some((major, minor, patch))
}
#[test]
fn test_parse_version() {
assert_eq!(parse_version(L!("3.5.2")), Some((3, 5, 2)));
assert_eq!(parse_version(L!("3.5.3beta")), Some((3, 5, 3)));
}
pub fn terminal_protocols_enable_ifn() {
let did_write = RelaxedAtomicBool::new(false);
let _save_screen_state = ScopeGuard::new((), |()| {
if did_write.load() {
reader_current_data().map(|data| data.save_screen_state());
}
});
let mut out = Outputter::stdoutput().borrow_mut();
if !BRACKETED_PASTE.load(Ordering::Relaxed) {
BRACKETED_PASTE.store(true, Ordering::Release);
out.write_command(DecsetBracketedPaste);
if IS_TMUX.load() {
out.write_command(DecsetFocusReporting);
}
did_write.store(true);
}
let kitty_keyboard_supported = KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed);
if kitty_keyboard_supported == Capability::Unknown as _ {
return;
}
if TERMINAL_PROTOCOLS.load(Ordering::Relaxed) {
return;
}
TERMINAL_PROTOCOLS.store(true, Ordering::Release);
FLOG!(term_protocols, "Enabling extended keys");
if kitty_keyboard_supported == Capability::NotSupported as _ || ITERM_NO_KITTY_KEYBOARD.load() {
out.write_command(ModifyOtherKeysEnable); // XTerm's modifyOtherKeys
out.write_command(ApplicationKeypadModeEnable); // set application keypad mode, so the keypad keys send unique codes
} else {
out.write_command(KittyKeyboardProgressiveEnhancementsEnable);
}
did_write.store(true);
}
pub(crate) fn terminal_protocols_disable_ifn() {
let did_write = RelaxedAtomicBool::new(false);
let _save_screen_state = is_main_thread().then(|| {
ScopeGuard::new((), |()| {
if did_write.load() {
reader_current_data().map(|data| data.save_screen_state());
}
})
});
let mut out = Outputter::stdoutput().borrow_mut();
if BRACKETED_PASTE.load(Ordering::Acquire) {
out.write_command(DecrstBracketedPaste);
if IS_TMUX.load() {
out.write_command(DecrstFocusReporting);
}
BRACKETED_PASTE.store(false, Ordering::Release);
did_write.store(true);
}
if !TERMINAL_PROTOCOLS.load(Ordering::Acquire) {
return;
}
FLOG_SAFE!(term_protocols, "Disabling extended keys");
let kitty_keyboard_supported = KITTY_KEYBOARD_SUPPORTED.load(Ordering::Acquire);
assert_ne!(kitty_keyboard_supported, Capability::Unknown as _);
if kitty_keyboard_supported == Capability::NotSupported as _ || ITERM_NO_KITTY_KEYBOARD.load() {
out.write_command(ModifyOtherKeysDisable);
out.write_command(ApplicationKeypadModeDisable);
} else {
out.write_command(KittyKeyboardProgressiveEnhancementsDisable);
}
TERMINAL_PROTOCOLS.store(false, Ordering::Release);
did_write.store(true);
}
fn parse_mask(mask: u32) -> (Modifiers, bool) {
let modifiers = Modifiers {
ctrl: (mask & 4) != 0,
@@ -814,7 +880,7 @@ fn try_readch(&mut self, blocking: bool) -> Option<CharEvent> {
return Some(mevt);
}
let rr = readb(self.get_in_fd(), blocking, /*pasting=*/ false);
let rr = readb(self.get_in_fd(), blocking);
match rr {
ReadbResult::Eof => {
return Some(CharEvent::Implicit(ImplicitEvent::Eof));
@@ -857,16 +923,10 @@ fn try_readch(&mut self, blocking: bool) -> Option<CharEvent> {
let mut i = 0;
let ok = loop {
if i == buffer.len() {
buffer.push(
match readb(
self.get_in_fd(),
/*blocking=*/ true,
/*pasting=*/ false,
) {
ReadbResult::Byte(b) => b,
_ => 0,
},
);
buffer.push(match readb(self.get_in_fd(), /*blocking=*/ true) {
ReadbResult::Byte(b) => b,
_ => 0,
});
}
match decode_input_byte(
&mut seq,
@@ -945,11 +1005,7 @@ fn try_readch(&mut self, blocking: bool) -> Option<CharEvent> {
}
fn try_readb(&mut self, buffer: &mut Vec<u8>) -> Option<u8> {
let ReadbResult::Byte(next) = readb(
self.get_in_fd(),
/*blocking=*/ false,
self.paste_is_buffering(),
) else {
let ReadbResult::Byte(next) = readb(self.get_in_fd(), /*blocking=*/ false) else {
return None;
};
buffer.push(next);
@@ -1009,7 +1065,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
// The maximum number of CSI parameters is defined by NPAR, nominally 16.
let mut params = [[0_u32; 4]; 16];
let Some(mut c) = self.try_readb(buffer) else {
return Some(KeyEvent::from(alt('[')));
return Some(KeyEvent::from(ctrl('[')));
};
let mut next_char = |zelf: &mut Self| zelf.try_readb(buffer).unwrap_or(0xff);
let private_mode;
@@ -1022,7 +1078,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
}
let mut count = 0;
let mut subcount = 0;
while count < 16 && (0x30..=0x3f).contains(&c) {
while count < 16 && c >= 0x30 && c <= 0x3f {
if c.is_ascii_digit() {
// Return None on invalid ascii numeric CSI parameter exceeding u32 bounds
match params[count][subcount]
@@ -1235,7 +1291,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
reader,
"Received kitty progressive enhancement flags, marking as supported"
);
set_kitty_keyboard_capability(reader_save_screen_state, Capability::Supported);
KITTY_KEYBOARD_SUPPORTED.store(Capability::Supported as _, Ordering::Release);
return None;
}
@@ -1469,12 +1525,50 @@ fn readch_timed(&mut self, wait_time_ms: usize) -> Option<CharEvent> {
if let Some(evt) = self.try_pop() {
return Some(evt);
}
terminal_protocols_enable_ifn();
check_fd_readable(
self.get_in_fd(),
Duration::from_millis(u64::try_from(wait_time_ms).unwrap()),
)
.then(|| self.readch())
// We are not prepared to handle a signal immediately; we only want to know if we get input on
// our fd before the timeout. Use pselect to block all signals; we will handle signals
// before the next call to readch().
let mut sigs = MaybeUninit::uninit();
unsafe { libc::sigfillset(sigs.as_mut_ptr()) };
// pselect expects timeouts in nanoseconds.
const NSEC_PER_MSEC: u64 = 1000 * 1000;
const NSEC_PER_SEC: u64 = NSEC_PER_MSEC * 1000;
let wait_nsec: u64 = (wait_time_ms as u64) * NSEC_PER_MSEC;
let timeout = libc::timespec {
tv_sec: (wait_nsec / NSEC_PER_SEC).try_into().unwrap(),
tv_nsec: (wait_nsec % NSEC_PER_SEC).try_into().unwrap(),
};
// We have one fd of interest.
let mut fdset = MaybeUninit::uninit();
let in_fd = self.get_in_fd();
unsafe {
libc::FD_ZERO(fdset.as_mut_ptr());
libc::FD_SET(in_fd, fdset.as_mut_ptr());
};
let res = unsafe {
libc::pselect(
in_fd + 1,
fdset.as_mut_ptr(),
ptr::null_mut(),
ptr::null_mut(),
&timeout,
sigs.as_ptr(),
)
};
// Prevent signal starvation on WSL causing the `torn_escapes.py` test to fail
if is_windows_subsystem_for_linux(WSL::V1) {
// Merely querying the current thread's sigmask is sufficient to deliver a pending signal
let _ = unsafe { libc::pthread_sigmask(0, ptr::null(), sigs.as_mut_ptr()) };
}
if res > 0 {
return Some(self.readch());
}
None
}
/// Return the fd from which to read.

View File

@@ -16,9 +16,6 @@
#![allow(clippy::incompatible_msrv)]
#![allow(clippy::len_without_is_empty)]
#![allow(clippy::manual_is_ascii_check)]
#![allow(clippy::manual_range_contains)]
#![allow(clippy::needless_lifetimes)]
#![allow(clippy::needless_return)]
#![allow(clippy::new_without_default)]
#![allow(clippy::option_map_unit_fn)]
#![allow(clippy::too_many_arguments)]
@@ -96,7 +93,6 @@
pub mod tokenizer;
pub mod topic_monitor;
pub mod trace;
pub mod tty_handoff;
pub mod universal_notifier;
pub mod util;
pub mod wait_handle;

View File

@@ -492,7 +492,7 @@ fn expand_command(
STATUS_UNMATCHED_WILDCARD,
statement,
WILDCARD_ERR_MSG,
self.node_source(statement)
&self.node_source(statement)
);
}
ExpandResultCode::cancel => {
@@ -1084,7 +1084,7 @@ fn run_switch_statement(
STATUS_UNMATCHED_WILDCARD,
&statement.argument,
WILDCARD_ERR_MSG,
self.node_source(&statement.argument)
&self.node_source(&statement.argument)
);
}
ExpandResultCode::ok => {
@@ -1370,7 +1370,7 @@ fn expand_arguments_from_nodes(
STATUS_UNMATCHED_WILDCARD,
arg_node,
WILDCARD_ERR_MSG,
self.node_source(*arg_node)
&self.node_source(*arg_node)
);
}
}
@@ -1420,7 +1420,7 @@ fn determine_redirections(
STATUS_INVALID_ARGS,
redir_node,
"Invalid redirection: %ls",
self.node_source(redir_node)
&self.node_source(redir_node)
);
}
};
@@ -1801,7 +1801,7 @@ fn populate_job_from_job_node(
STATUS_INVALID_ARGS,
&jc.pipe,
ILLEGAL_FD_ERR_MSG,
self.node_source(&jc.pipe)
&self.node_source(&jc.pipe)
);
break;
}

View File

@@ -14,7 +14,7 @@
};
use crate::fds::{open_dir, BEST_O_SEARCH};
use crate::global_safety::RelaxedAtomicBool;
use crate::input_common::TerminalQuery;
use crate::input_common::{terminal_protocols_disable_ifn, TerminalQuery};
use crate::io::IoChain;
use crate::job_group::MaybeJobId;
use crate::operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT};
@@ -604,7 +604,7 @@ pub fn eval_file_wstr(
let sb = self.push_block(Block::source_block(filename.clone()));
let _filename_push = self
.library_data
.scoped_set(Some(filename), |s| &mut s.current_filename);
.scoped_set(Some(filename.clone()), |s| &mut s.current_filename);
let ret = self.eval_wstr(src, io, job_group, BlockType::top);
@@ -681,6 +681,8 @@ pub fn eval_node<T: Node>(
// Create a new execution context.
let mut execution_context = ExecutionContext::new(ps, block_io.clone(), &self.line_counter);
terminal_protocols_disable_ifn();
// Check the exec count so we know if anything got executed.
let prev_exec_count = self.libdata().exec_count;
let prev_status_count = self.libdata().status_count;

View File

@@ -6,6 +6,8 @@
use crate::env::{EnvMode, EnvStack, Environment};
use crate::expand::{expand_tilde, HOME_DIRECTORY};
use crate::flog::{FLOG, FLOGF};
#[cfg(not(target_os = "linux"))]
use crate::libc::{MNT_LOCAL, ST_LOCAL};
use crate::wchar::prelude::*;
use crate::wutil::{normalize_path, path_normalize_for_cd, waccess, wdirname, wstat};
use errno::{errno, set_errno, Errno};
@@ -703,49 +705,25 @@ pub fn path_remoteness(path: &wstr) -> DirRemoteness {
}
#[cfg(not(target_os = "linux"))]
{
fn remoteness_via_statfs<StatFS, Flags>(
statfn: unsafe extern "C" fn(*const i8, *mut StatFS) -> libc::c_int,
flagsfn: fn(&StatFS) -> Flags,
is_local_flag: u64,
path: &std::ffi::CStr,
) -> DirRemoteness
where
u64: From<Flags>,
{
if is_local_flag == 0 {
return DirRemoteness::unknown;
}
// ST_LOCAL is a flag to statvfs, which is itself standardized.
// In practice the only system to define it is NetBSD.
let local_flag = ST_LOCAL() | MNT_LOCAL();
if local_flag != 0 {
let mut buf = MaybeUninit::uninit();
if unsafe { (statfn)(path.as_ptr(), buf.as_mut_ptr()) } < 0 {
if unsafe { libc::statfs(narrow.as_ptr(), buf.as_mut_ptr()) } < 0 {
return DirRemoteness::unknown;
}
let buf = unsafe { buf.assume_init() };
// statfs::f_flag is hard-coded as 64-bits on 32/64-bit FreeBSD but it's a (4-byte)
// long on 32-bit NetBSD.. and always 4-bytes on macOS (even on 64-bit builds).
#[allow(clippy::useless_conversion)]
if u64::from((flagsfn)(&buf)) & is_local_flag != 0 {
return if u64::from(buf.f_flags) & local_flag != 0 {
DirRemoteness::local
} else {
DirRemoteness::remote
}
};
}
// ST_LOCAL is a flag to statvfs, which is itself standardized.
// In practice the only system to define it is NetBSD.
#[cfg(target_os = "netbsd")]
let remoteness = remoteness_via_statfs(
libc::statvfs,
|stat: &libc::statvfs| stat.f_flag,
crate::libc::ST_LOCAL(),
&narrow,
);
#[cfg(not(target_os = "netbsd"))]
let remoteness = remoteness_via_statfs(
libc::statfs,
|stat: &libc::statfs| stat.f_flags,
crate::libc::MNT_LOCAL(),
&narrow,
);
remoteness
DirRemoteness::unknown
}
}

View File

@@ -17,15 +17,17 @@
use crate::reader::{fish_is_unwinding_for_exit, reader_schedule_prompt_repaint};
use crate::redirection::RedirectionSpecList;
use crate::signal::{signal_set_handlers_once, Signal};
use crate::threads::MainThread;
use crate::topic_monitor::{topic_monitor_principal, GenerationsList, Topic};
use crate::wait_handle::{InternalJobId, WaitHandle, WaitHandleRef, WaitHandleStore};
use crate::wchar::prelude::*;
use crate::wchar_ext::ToWString;
use crate::wutil::{wbasename, wperror};
use crate::wutil::{perror, wbasename, wperror};
use libc::{
EXIT_SUCCESS, SIGABRT, SIGBUS, SIGCONT, SIGFPE, SIGHUP, SIGILL, SIGINT, SIGKILL, SIGPIPE,
SIGQUIT, SIGSEGV, SIGSYS, SIGTTOU, SIG_DFL, SIG_IGN, WCONTINUED, WEXITSTATUS, WIFCONTINUED,
WIFEXITED, WIFSIGNALED, WIFSTOPPED, WNOHANG, WTERMSIG, WUNTRACED, _SC_CLK_TCK,
EBADF, EINVAL, ENOTTY, EPERM, EXIT_SUCCESS, SIGABRT, SIGBUS, SIGCONT, SIGFPE, SIGHUP, SIGILL,
SIGINT, SIGKILL, SIGPIPE, SIGQUIT, SIGSEGV, SIGSYS, SIGTTOU, SIG_DFL, SIG_IGN, STDIN_FILENO,
WCONTINUED, WEXITSTATUS, WIFCONTINUED, WIFEXITED, WIFSIGNALED, WIFSTOPPED, WNOHANG, WTERMSIG,
WUNTRACED, _SC_CLK_TCK,
};
use once_cell::sync::Lazy;
#[cfg(not(target_has_atomic = "64"))]
@@ -33,13 +35,14 @@
use std::cell::{Cell, Ref, RefCell, RefMut};
use std::fs;
use std::io::{Read, Write};
use std::mem::MaybeUninit;
use std::num::NonZeroU32;
use std::os::fd::RawFd;
use std::rc::Rc;
#[cfg(target_has_atomic = "64")]
use std::sync::atomic::AtomicU64;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::sync::{Arc, OnceLock};
/// Types of processes.
#[derive(Default)]
@@ -267,6 +270,202 @@ pub fn get_id(&self) -> u64 {
}
}
// Allows transferring the tty to a job group, while it runs.
#[derive(Default)]
pub struct TtyTransfer {
// The job group which owns the tty, or empty if none.
owner: Option<JobGroupRef>,
}
impl TtyTransfer {
pub fn new() -> Self {
Default::default()
}
/// Transfer to the given job group, if it wants to own the terminal.
#[allow(clippy::wrong_self_convention)]
pub fn to_job_group(&mut self, jg: &JobGroupRef) {
assert!(self.owner.is_none(), "Terminal already transferred");
if TtyTransfer::try_transfer(jg) {
self.owner = Some(jg.clone());
}
}
/// Reclaim the tty if we transferred it.
pub fn reclaim(&mut self) {
if self.owner.is_some() {
FLOG!(proc_pgroup, "fish reclaiming terminal");
if unsafe { libc::tcsetpgrp(STDIN_FILENO, libc::getpgrp()) } == -1 {
FLOG!(warning, "Could not return shell to foreground");
perror("tcsetpgrp");
}
self.owner = None;
}
}
/// Save the current tty modes into the owning job group, if we are transferred.
pub fn save_tty_modes(&mut self) {
if let Some(ref mut owner) = self.owner {
let mut tmodes = MaybeUninit::uninit();
if unsafe { libc::tcgetattr(STDIN_FILENO, tmodes.as_mut_ptr()) } == 0 {
owner.tmodes.replace(Some(unsafe { tmodes.assume_init() }));
} else if errno::errno().0 != ENOTTY {
perror("tcgetattr");
}
}
}
fn try_transfer(jg: &JobGroup) -> bool {
if !jg.wants_terminal() {
// The job doesn't want the terminal.
return false;
}
// Get the pgid; we must have one if we want the terminal.
let pgid = jg.get_pgid().unwrap();
// It should never be fish's pgroup.
let fish_pgrp = crate::nix::getpgrp();
assert!(
pgid.as_pid_t() != fish_pgrp,
"Job should not have fish's pgroup"
);
// Ok, we want to transfer to the child.
// Note it is important to be very careful about calling tcsetpgrp()!
// fish ignores SIGTTOU which means that it has the power to reassign the tty even if it doesn't
// own it. This means that other processes may get SIGTTOU and become zombies.
// Check who own the tty now. There's four cases of interest:
// 1. There is no tty at all (tcgetpgrp() returns -1). For example running from a pure script.
// Of course do not transfer it in that case.
// 2. The tty is owned by the process. This comes about often, as the process will call
// tcsetpgrp() on itself between fork and exec. This is the essential race inherent in
// tcsetpgrp(). In this case we want to reclaim the tty, but do not need to transfer it
// ourselves since the child won the race.
// 3. The tty is owned by a different process. This may come about if fish is running in the
// background with job control enabled. Do not transfer it.
// 4. The tty is owned by fish. In that case we want to transfer the pgid.
let current_owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
if current_owner < 0 {
// Case 1.
return false;
} else if current_owner == pgid.get() {
// Case 2.
return true;
} else if current_owner != pgid.get() && current_owner != fish_pgrp {
// Case 3.
return false;
}
// Case 4 - we do want to transfer it.
// The tcsetpgrp(2) man page says that EPERM is thrown if "pgrp has a supported value, but
// is not the process group ID of a process in the same session as the calling process."
// Since we _guarantee_ that this isn't the case (the child calls setpgid before it calls
// SIGSTOP, and the child was created in the same session as us), it seems that EPERM is
// being thrown because of an caching issue - the call to tcsetpgrp isn't seeing the
// newly-created process group just yet. On this developer's test machine (WSL running Linux
// 4.4.0), EPERM does indeed disappear on retry. The important thing is that we can
// guarantee the process isn't going to exit while we wait (which would cause us to possibly
// block indefinitely).
while unsafe { libc::tcsetpgrp(STDIN_FILENO, pgid.as_pid_t()) } != 0 {
FLOGF!(proc_termowner, "tcsetpgrp failed: %d", errno::errno().0);
// Before anything else, make sure that it's even necessary to call tcsetpgrp.
// Since it usually _is_ necessary, we only check in case it fails so as to avoid the
// unnecessary syscall and associated context switch, which profiling has shown to have
// a significant cost when running process groups in quick succession.
let getpgrp_res = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
if getpgrp_res < 0 {
match errno::errno().0 {
ENOTTY | EBADF => {
// stdin is not a tty. This may come about if job control is enabled but we are
// not a tty - see #6573.
return false;
}
_ => {
perror("tcgetpgrp");
return false;
}
}
}
if getpgrp_res == pgid.get() {
FLOGF!(
proc_termowner,
"Process group %d already has control of terminal",
pgid
);
return true;
}
let pgroup_terminated;
if errno::errno().0 == EINVAL {
// OS X returns EINVAL if the process group no longer lives. Probably other OSes,
// too. Unlike EPERM below, EINVAL can only happen if the process group has
// terminated.
pgroup_terminated = true;
} else if errno::errno().0 == EPERM {
// Retry so long as this isn't because the process group is dead.
let mut result: libc::c_int = 0;
let wait_result = unsafe { libc::waitpid(-pgid.as_pid_t(), &mut result, WNOHANG) };
if wait_result == -1 {
// Note that -1 is technically an "error" for waitpid in the sense that an
// invalid argument was specified because no such process group exists any
// longer. This is the observed behavior on Linux 4.4.0. a "success" result
// would mean processes from the group still exist but is still running in some
// state or the other.
pgroup_terminated = true;
} else {
// Debug the original tcsetpgrp error (not the waitpid errno) to the log, and
// then retry until not EPERM or the process group has exited.
FLOGF!(
proc_termowner,
"terminal_give_to_job(): EPERM with pgid %d.",
pgid
);
continue;
}
} else if errno::errno().0 == ENOTTY {
// stdin is not a TTY. In general we expect this to be caught via the tcgetpgrp
// call's EBADF handler above.
return false;
} else {
FLOGF!(
warning,
"Could not send job %d ('%ls') with pgid %d to foreground",
jg.job_id.to_wstring(),
jg.command,
pgid
);
perror("tcsetpgrp");
return false;
}
if pgroup_terminated {
// All processes in the process group has exited.
// Since we delay reaping any processes in a process group until all members of that
// job/group have been started, the only way this can happen is if the very last
// process in the group terminated and didn't need to access the terminal, otherwise
// it would have hung waiting for terminal IO (SIGTTIN). We can safely ignore this.
FLOGF!(
proc_termowner,
"tcsetpgrp called but process group %d has terminated.\n",
pgid
);
return false;
}
break;
}
true
}
}
/// The destructor will assert if reclaim() has not been called.
impl Drop for TtyTransfer {
fn drop(&mut self) {
assert!(self.owner.is_none(), "Forgot to reclaim() the tty");
}
}
/// A type-safe equivalent to [`libc::pid_t`].
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
@@ -521,15 +720,19 @@ pub fn is_completed(&self) -> bool {
/// As a process does not know its job id, we pass it in.
/// Note this will return null if the process is not waitable (has no pid).
pub fn make_wait_handle(&self, jid: InternalJobId) -> Option<WaitHandleRef> {
let pid = self.pid()?;
if self.wait_handle.borrow().is_none() {
self.wait_handle.replace(Some(WaitHandle::new(
pid,
jid,
wbasename(&self.actual_cmd.clone()).to_owned(),
)));
if !matches!(self.typ, ProcessType::External) || self.pid().is_none() {
// Not waitable.
None
} else {
if self.wait_handle.borrow().is_none() {
self.wait_handle.replace(Some(WaitHandle::new(
self.pid().unwrap(),
jid,
wbasename(&self.actual_cmd.clone()).to_owned(),
)));
}
self.get_wait_handle()
}
self.get_wait_handle()
}
}
@@ -1146,7 +1349,7 @@ pub fn hup_jobs(jobs: &JobList) {
/// Add a job to the list of PIDs/PGIDs we wait on even though they are not associated with any
/// jobs. Used to avoid zombie processes after disown.
pub fn add_disowned_job(j: &Job) {
let mut disowned_pids = DISOWNED_PIDS.lock().unwrap();
let mut disowned_pids = DISOWNED_PIDS.get().borrow_mut();
for process in j.external_procs() {
disowned_pids.push(process.pid().unwrap());
}
@@ -1154,7 +1357,7 @@ pub fn add_disowned_job(j: &Job) {
// Reap any pids in our disowned list that have exited. This is used to avoid zombies.
fn reap_disowned_pids() {
let mut disowned_pids = DISOWNED_PIDS.lock().unwrap();
let mut disowned_pids = DISOWNED_PIDS.get().borrow_mut();
// waitpid returns 0 iff the PID/PGID in question has not changed state; remove the pid/pgid
// if it has changed or an error occurs (presumably ECHILD because the child does not exist)
disowned_pids.retain(|pid| {
@@ -1169,7 +1372,7 @@ fn reap_disowned_pids() {
/// A list of pids that have been disowned. They are kept around until either they exit or
/// we exit. Poll these from time-to-time to prevent zombie processes from happening (#5342).
static DISOWNED_PIDS: Mutex<Vec<Pid>> = Mutex::new(Vec::new());
static DISOWNED_PIDS: MainThread<RefCell<Vec<Pid>>> = MainThread::new(RefCell::new(Vec::new()));
/// See if any reapable processes have exited, and mark them accordingly.
/// \param block_ok if no reapable processes have exited, block until one is (or until we receive a

View File

@@ -11,16 +11,11 @@
//! When the user searches forward, i.e. presses Alt-down, the list is consulted for previous search
//! result, and subsequent backwards searches are also handled by consulting the list up until the
//! end of the list is reached, at which point regular searching will commence.
//!
//! In general interactive reads work with the tty protocols (CSI-U, etc) enabled; these are disabled
//! before calling out to fish script, wildcards, or completions. Note CSI-U protocol prevents
//! control-C from generating SIGINT, so failing to disable these would prevent cancellation of wildcard
//! expansion, etc.
use libc::{
c_char, ECHO, EINTR, EIO, EISDIR, ENOTTY, EPERM, ESRCH, ICANON, ICRNL, IEXTEN, INLCR, IXOFF,
IXON, ONLCR, OPOST, O_NONBLOCK, O_RDONLY, SIGINT, SIGTTIN, STDERR_FILENO, STDIN_FILENO,
STDOUT_FILENO, TCSANOW, VMIN, VQUIT, VSUSP, VTIME, _POSIX_VDISABLE,
IXON, ONLCR, OPOST, O_NONBLOCK, O_RDONLY, SIGINT, SIGTTIN, STDIN_FILENO, STDOUT_FILENO,
TCSANOW, VMIN, VQUIT, VSUSP, VTIME, _POSIX_VDISABLE,
};
use nix::fcntl::OFlag;
use nix::sys::stat::Mode;
@@ -86,9 +81,17 @@
SearchType,
};
use crate::input::init_input;
use crate::input_common::stop_query;
use crate::input_common::terminal_protocols_disable_ifn;
use crate::input_common::CursorPositionQuery;
use crate::input_common::ImplicitEvent;
use crate::input_common::QueryResponseEvent;
use crate::input_common::TerminalQuery;
use crate::input_common::IN_DVTM;
use crate::input_common::IN_MIDNIGHT_COMMANDER;
use crate::input_common::{
stop_query, CharEvent, CharInputStyle, CursorPositionQuery, ImplicitEvent, InputData,
QueryResponseEvent, ReadlineCmd, TerminalQuery,
terminal_protocol_hacks, terminal_protocols_enable_ifn, CharEvent, CharInputStyle, InputData,
ReadlineCmd,
};
use crate::io::IoChain;
use crate::key::ViewportPosition;
@@ -131,7 +134,9 @@
QueryCursorPosition, QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute,
QueryXtgettcap, QueryXtversion,
};
use crate::terminal::{Capability, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE};
use crate::terminal::{
Capability, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE,
};
use crate::termsize::{termsize_invalidate_tty, termsize_last, termsize_update};
use crate::text_face::parse_text_face;
use crate::text_face::TextFace;
@@ -145,10 +150,6 @@
tok_command, MoveWordStateMachine, MoveWordStyle, TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED,
TOK_SHOW_COMMENTS,
};
use crate::tty_handoff::{
get_kitty_keyboard_capability, get_tty_protocols_active, initialize_tty_metadata,
safe_deactivate_tty_protocols, set_kitty_keyboard_capability, tty_metadata, TtyHandoff,
};
use crate::wchar::prelude::*;
use crate::wcstringutil::string_prefixes_string_maybe_case_insensitive;
use crate::wcstringutil::{
@@ -231,6 +232,7 @@ fn debounce_history_pager() -> &'static Debounce {
}
fn redirect_tty_after_sighup() {
use libc::{EIO, ENOTTY, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
use std::fs::OpenOptions;
// If we have received SIGHUP, redirect the tty to avoid a user script triggering SIGTTIN or
@@ -263,8 +265,10 @@ pub(crate) fn initial_query(
vars: Option<&dyn Environment>,
) {
blocking_query.get_or_init(|| {
let md = tty_metadata();
let query = if is_dumb() || md.in_midnight_commander || md.in_dvtm || !isatty(STDOUT_FILENO)
let query = if is_dumb()
|| IN_MIDNIGHT_COMMANDER.load()
|| IN_DVTM.load()
|| !isatty(STDOUT_FILENO)
{
None
} else {
@@ -314,12 +318,11 @@ pub fn reader_push<'a>(parser: &'a Parser, history_name: &wstr, conf: ReaderConf
assert_is_main_thread();
let hist = History::with_name(history_name);
hist.resolve_pending();
let is_top_level = reader_data_stack().is_empty();
let data = ReaderData::new(hist, conf, is_top_level);
let data = ReaderData::new(hist, conf);
reader_data_stack().push(data);
let data = current_data().unwrap();
data.command_line_changed(EditableLineTag::Commandline, AutosuggestionUpdate::Remove);
if is_top_level {
if reader_data_stack().len() == 1 {
reader_interactive_init(parser);
}
Reader { data, parser }
@@ -398,6 +401,8 @@ pub struct CommandlineState {
pub search_field: Option<(WString, usize)>,
/// pager is visible and search is active
pub search_mode: bool,
/// if false, the reader has not yet been entered
pub initialized: bool,
}
impl CommandlineState {
@@ -411,6 +416,7 @@ const fn new() -> Self {
pager_fully_disclosed: false,
search_field: None,
search_mode: false,
initialized: false,
}
}
}
@@ -713,11 +719,6 @@ fn read_i(parser: &Parser) {
let mut data = reader_push(parser, &history_session_id(parser.vars()), conf);
data.import_history_if_necessary();
// Set up tty protocols. These should be enabled while we're reading interactively,
// and disabled before we run fish script, wildcards, or completions. This is scoped.
// Note this may be disabled within the loop, e.g. when running fish script bound to keys.
let mut tty = TtyHandoff::new(reader_save_screen_state);
while !check_exit_loop_maybe_warning(Some(&mut data)) {
RUN_COUNT.fetch_add(1, Ordering::Relaxed);
@@ -729,8 +730,6 @@ fn read_i(parser: &Parser) {
continue;
}
// Got a command. Disable tty protocols while we execute it.
tty.disable_tty_protocols();
data.clear(EditableLineTag::Commandline);
data.update_buff_pos(EditableLineTag::Commandline, None);
BufferedOutputter::new(Outputter::stdoutput()).write_command(Osc133CommandStart(&command));
@@ -766,8 +765,7 @@ fn read_i(parser: &Parser) {
}
reader_pop();
// If we got SIGHUP, ensure the tty is redirected and release tty handoff without
// trying to muck with protocols.
// If we got SIGHUP, ensure the tty is redirected.
if reader_received_sighup() {
// If we are the top-level reader, then we translate SIGHUP into exit_forced.
redirect_tty_after_sighup();
@@ -901,9 +899,11 @@ pub fn reader_init(will_restore_foreground_pgroup: bool) {
}
}
// TODO(pca): this is run in our "AT_EXIT" handler from a SIGTERM handler.
// It must be made async-signal-safe (or not invoked).
pub fn reader_deinit(restore_foreground_pgroup: bool) {
safe_restore_term_mode();
safe_deactivate_tty_protocols();
crate::input_common::terminal_protocols_disable_ifn();
if restore_foreground_pgroup {
restore_term_foreground_process_group_for_exit();
}
@@ -1205,18 +1205,12 @@ fn reader_received_sighup() -> bool {
}
impl ReaderData {
fn new(history: Arc<History>, conf: ReaderConfig, is_top_level: bool) -> Pin<Box<Self>> {
fn new(history: Arc<History>, conf: ReaderConfig) -> Pin<Box<Self>> {
let input_data = InputData::new(conf.inputfd);
let mut command_line = EditableLine::default();
if is_top_level {
let state = commandline_state_snapshot();
command_line.push_edit(Edit::new(0..0, state.text.clone()), false);
command_line.set_position(state.cursor_pos);
}
Pin::new(Box::new(Self {
canary: Rc::new(()),
conf,
command_line,
command_line: Default::default(),
command_line_transient_edit: None,
rendered_layout: Default::default(),
autosuggestion: Default::default(),
@@ -1377,6 +1371,7 @@ fn update_commandline_state(&self) {
});
}
snapshot.search_mode = self.history_search.active();
snapshot.initialized = true;
}
/// Apply any changes from the reader snapshot. This is called after running fish script,
@@ -1484,10 +1479,6 @@ pub fn mouse_left_click(&mut self, cursor: ViewportPosition, click_position: Vie
}
}
pub fn reader_save_screen_state() {
current_data().map(|data| data.save_screen_state());
}
/// Given a command line and an autosuggestion, return the string that gets shown to the user.
/// Exposed for testing purposes only.
pub fn combine_command_and_autosuggestion(
@@ -1502,7 +1493,7 @@ pub fn combine_command_and_autosuggestion(
assert!(!autosuggestion.is_empty());
assert!(autosuggestion.len() >= line_range.len());
let available = autosuggestion.len() - line_range.len();
let line = &cmdline[line_range];
let line = &cmdline[line_range.clone()];
if !string_prefixes_string(line, autosuggestion) {
// We have an autosuggestion which is not a prefix of the command line, i.e. a case
@@ -2186,8 +2177,6 @@ impl<'a> Reader<'a> {
/// Read a command to execute, respecting input bindings.
/// Return the command, or none if we were asked to cancel (e.g. SIGHUP).
fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
let mut tty = TtyHandoff::new(reader_save_screen_state);
self.rls = Some(ReadlineLoopState::new());
// Suppress fish_trace during executing key bindings.
@@ -2255,18 +2244,11 @@ fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
self.force_exec_prompt_and_repaint = true;
while !self.rls().finished && !check_exit_loop_maybe_warning(Some(self)) {
// Enable tty protocols while we read input.
tty.enable_tty_protocols();
if self.handle_char_event(None).is_break() {
break;
}
}
// Disable tty protocols now that we're going to execute a command.
if tty.disable_tty_protocols() {
self.save_screen_state();
}
if self.conf.transient_prompt {
self.exec_prompt(true, true);
}
@@ -2326,16 +2308,10 @@ fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
fn eval_bind_cmd(&mut self, cmd: &wstr) {
let last_statuses = self.parser.vars().get_last_statuses();
let prev_exec_external_count = self.parser.libdata().exec_external_count;
// Disable TTY protocols while we run a bind command, because it may call out.
let mut scoped_tty = TtyHandoff::new(reader_save_screen_state);
let mut modified_tty = scoped_tty.disable_tty_protocols();
self.parser.eval(cmd, &IoChain::new());
self.parser.set_last_statuses(last_statuses);
modified_tty |= scoped_tty.reclaim();
if modified_tty
|| (self.parser.libdata().exec_external_count != prev_exec_external_count
&& self.data.left_prompt_buff.contains('\n'))
if self.parser.libdata().exec_external_count != prev_exec_external_count
&& self.data.left_prompt_buff.contains('\n')
{
self.save_screen_state();
}
@@ -2377,6 +2353,7 @@ fn read_normal_chars(&mut self) -> Option<CharEvent> {
let mut accumulated_chars = WString::new();
while accumulated_chars.len() < limit {
terminal_protocols_enable_ifn();
let evt = self.read_char();
let CharEvent::Key(kevt) = &evt else {
event_needing_handling = Some(evt);
@@ -2570,11 +2547,11 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
// Rogue reply.
return ControlFlow::Continue(());
}
if get_kitty_keyboard_capability() == Capability::Unknown {
set_kitty_keyboard_capability(
reader_save_screen_state,
Capability::NotSupported,
);
if KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed)
== Capability::Unknown as _
{
KITTY_KEYBOARD_SUPPORTED
.store(Capability::NotSupported as _, Ordering::Release);
}
}
QueryResponseEvent::CursorPositionReport(cursor_pos) => {
@@ -2614,8 +2591,6 @@ fn send_xtgettcap_query(out: &mut impl Output, cap: &'static str) {
out.write_command(QueryXtgettcap(cap));
}
#[allow(renamed_and_removed_lints)]
#[allow(clippy::blocks_in_if_conditions)] // for old clippy
fn query_capabilities_via_dcs(out: &mut impl Output, vars: &dyn Environment) {
if vars.get_unless_empty(L!("STY")).is_some()
|| vars.get_unless_empty(L!("TERM")).is_some_and(|term| {
@@ -2821,14 +2796,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
}
} else {
// Either the user hit tab only once, or we had no visible completion list.
// Disable tty protocols while we compute completions, so that control-C
// triggers SIGINT (suppressed by CSI-U).
let mut tty = TtyHandoff::new(reader_save_screen_state);
tty.disable_tty_protocols();
self.compute_and_apply_completions(c);
if tty.reclaim() {
self.save_screen_state();
}
}
}
rl::PagerToggleSearch => {
@@ -3602,33 +3570,6 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
self.update_buff_pos(elt, Some(buff_pos));
}
}
rl::UpcaseSelection | rl::DowncaseSelection => {
let (elt, el) = self.active_edit_line();
// Check that we have an active selection and get the bounds.
if let Some(selection) = self.get_selection() {
let text = &el.text().as_char_slice()[selection.clone()];
let replacement = if c == rl::UpcaseSelection {
WString::from_iter(text.iter().flat_map(|c| c.to_uppercase()))
} else {
WString::from_iter(text.iter().flat_map(|c| c.to_lowercase()))
};
let buff_pos = el.position();
self.replace_substring(elt, selection, replacement);
// Restore the buffer position since replace_substring moves
// the buffer position ahead of the replaced text.
// Note: This does not take string length changes into account.
// E.g.: When the cursor was at the right of the selection,
// the selection contains 'ẞ', which is uppercased into 'SS',
// the cursor will stay at the same offset, but it will not be on the same
// character as before.
// The position calculations work on codepoints rather than graphemes, which can
// result in additional issues.
self.update_buff_pos(elt, Some(buff_pos));
}
}
rl::UpcaseWord | rl::DowncaseWord | rl::CapitalizeWord => {
let (elt, el) = self.active_edit_line();
// For capitalize_word, whether we've capitalized a character so far.
@@ -4523,7 +4464,7 @@ fn reader_interactive_init(parser: &Parser) {
.vars()
.set_one(L!("_"), EnvMode::GLOBAL, L!("fish").to_owned());
initialize_tty_metadata();
terminal_protocol_hacks();
}
/// Destroy data for interactive use.
@@ -4619,10 +4560,6 @@ fn exec_prompt(&mut self, full_prompt: bool, final_prompt: bool) {
// Prompts must be run non-interactively.
let _noninteractive = self.parser.push_scope(|s| s.is_interactive = false);
// Suppress TTY protocols in a scoped way so that e.g. control-C can cancel the prompt.
let mut scoped_tty = TtyHandoff::new(reader_save_screen_state);
scoped_tty.disable_tty_protocols();
// Update the termsize now.
// This allows prompts to react to $COLUMNS.
self.update_termsize();
@@ -4872,7 +4809,7 @@ fn get_autosuggestion_performer(
};
let mut result = AutosuggestionResult::new(
command_line,
search_string_range,
search_string_range.clone(),
suggestion,
true, // normal completions are case-insensitive
/*is_whole_item_from_history=*/ false,
@@ -5716,10 +5653,6 @@ fn check_for_orphaned_process(loop_count: usize, shell_pgid: libc::pid_t) -> boo
/// Run the specified command with the correct terminal modes, and while taking care to perform job
/// notification, set the title, etc.
fn reader_run_command(parser: &Parser, cmd: &wstr) -> EvalRes {
assert!(
!get_tty_protocols_active(),
"TTY protocols should not be active"
);
let ft = tok_command(cmd);
// Provide values for `status current-command` and `status current-commandline`
@@ -6227,7 +6160,6 @@ pub fn completion_apply_to_command_line(
&& unescaped_quote(command_line, insertion_point) != quote
{
// This is a quoted parameter, first print a quote.
#[allow(clippy::unnecessary_unwrap)] // for old clippy
result.insert(new_cursor_pos, quote.unwrap());
new_cursor_pos += 1;
}
@@ -6272,10 +6204,6 @@ fn compute_and_apply_completions(&mut self, c: ReadlineCmd) {
c,
ReadlineCmd::Complete | ReadlineCmd::CompleteAndSearch
));
assert!(
!get_tty_protocols_active(),
"should not be called with TTY protocols active"
);
// Remove a trailing backslash. This may trigger an extra repaint, but this is
// rare.
@@ -6305,6 +6233,9 @@ fn compute_and_apply_completions(&mut self, c: ReadlineCmd) {
token_range.start += cmdsub_range.start;
token_range.end += cmdsub_range.start;
// Wildcard expansion and completion below check for cancellation.
terminal_protocols_disable_ifn();
// Check if we have a wildcard within this string; if so we first attempt to expand the
// wildcard; if that succeeds we don't then apply user completions (#8593).
let mut wc_expanded = WString::new();

View File

@@ -7,7 +7,6 @@
use crate::reader::{reader_handle_sigint, reader_sighup, safe_restore_term_mode};
use crate::termsize::TermsizeContainer;
use crate::topic_monitor::{topic_monitor_principal, Generation, GenerationsList, Topic};
use crate::tty_handoff::{safe_deactivate_tty_protocols, safe_mark_tty_invalid};
use crate::wchar::prelude::*;
use crate::wutil::{fish_wcstoi, perror};
use errno::{errno, set_errno};
@@ -84,15 +83,13 @@ extern "C" fn fish_signal_handler(
// Exit unless the signal was trapped.
if !observed {
reader_sighup();
safe_mark_tty_invalid();
}
topic_monitor_principal().post(Topic::sighupint);
}
libc::SIGTERM => {
// Handle sigterm. The only thing we do is restore the front process ID and disable protocols, then die.
// Handle sigterm. The only thing we do is restore the front process ID, then die.
if !observed {
safe_restore_term_mode();
safe_deactivate_tty_protocols();
// Safety: signal() and raise() are async-signal-safe.
unsafe {
libc::signal(libc::SIGTERM, libc::SIG_DFL);

View File

@@ -216,14 +216,15 @@ fn maybe_terminfo(
true
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum Capability {
pub(crate) enum Capability {
Unknown,
Supported,
NotSupported,
}
pub(crate) static KITTY_KEYBOARD_SUPPORTED: AtomicU8 = AtomicU8::new(Capability::Unknown as _);
pub(crate) static SCROLL_FORWARD_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub(crate) static SCROLL_FORWARD_TERMINFO_CODE: &str = "indn";
@@ -877,7 +878,7 @@ pub fn setup() {
if let Ok(result) = res {
// Create a new `Term` instance, prepopulate the capabilities we care about.
let term = Arc::new(Term::new(result));
*global_term = Some(term);
*global_term = Some(term.clone());
} else {
*global_term = None;
}

View File

@@ -63,7 +63,7 @@ fn callback(&self, fd: &mut AutoCloseFd) {
let mut buf = [0u8; 1024];
let res = nix::unistd::read(&fd, &mut buf);
let amt = res.expect("read error!");
self.length_read.fetch_add(amt, Ordering::Relaxed);
self.length_read.fetch_add(amt as usize, Ordering::Relaxed);
let was_closed = amt == 0;
self.total_calls.fetch_add(1, Ordering::Relaxed);

View File

@@ -702,7 +702,6 @@ fn test_expand_argument_list() {
fn test_1_cancellation(parser: &Parser, src: &wstr) {
let filler = IoBufferfill::create().unwrap();
let delay = Duration::from_millis(100);
#[allow(clippy::unnecessary_cast)]
let thread = unsafe { libc::pthread_self() } as usize;
iothread_perform(move || {
// Wait a while and then SIGINT the main thread.

View File

@@ -1,606 +0,0 @@
//! Utility for transferring the tty to a child process in a scoped way,
//! and reclaiming it after.
use crate::common::{self, safe_write_loop};
use crate::flog::{FLOG, FLOGF};
use crate::global_safety::RelaxedAtomicBool;
use crate::job_group::JobGroup;
use crate::proc::JobGroupRef;
use crate::terminal::TerminalCommand::{
self, ApplicationKeypadModeDisable, ApplicationKeypadModeEnable, DecrstBracketedPaste,
DecrstFocusReporting, DecsetBracketedPaste, DecsetFocusReporting,
KittyKeyboardProgressiveEnhancementsDisable, KittyKeyboardProgressiveEnhancementsEnable,
ModifyOtherKeysDisable, ModifyOtherKeysEnable,
};
use crate::terminal::{Capability, Output, Outputter};
use crate::threads::assert_is_main_thread;
use crate::wchar_ext::ToWString;
use crate::wutil::perror;
use libc::{EINVAL, ENOTTY, EPERM, STDIN_FILENO, WNOHANG};
use std::mem::MaybeUninit;
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU8, Ordering};
// Facts about our environment, which inform how we handle the tty.
#[derive(Debug, Copy, Clone)]
pub struct TtyMetadata {
// Whether we are running under Midnight Commander.
pub in_midnight_commander: bool,
// Whether we are running under dvtm.
pub in_dvtm: bool,
// Whether we are running under tmux.
pub in_tmux: bool,
// If set, we are running before iTerm2 3.5.12, which does not support CSI-U.
pub pre_kitty_iterm2: bool,
}
impl TtyMetadata {
// Create a new TtyMetadata instance with the current environment.
fn detect() -> Self {
use std::env::{var, var_os};
let in_midnight_commander = var_os("MC_TMPDIR").is_some();
let in_dvtm = var("TERM").as_deref() == Ok("dvtm-256color");
let in_tmux = var_os("TMUX").is_some();
// Detect iTerm2 before 3.5.12.
let pre_kitty_iterm2 = get_iterm2_version().is_some_and(|v| v < (3, 5, 12));
Self {
in_midnight_commander,
in_dvtm,
in_tmux,
pre_kitty_iterm2,
}
}
}
// Whether CSI-U ("Kitty") support is present in the TTY.
static KITTY_KEYBOARD_SUPPORTED: AtomicU8 = AtomicU8::new(Capability::Unknown as _);
// Get the support capability for CSI-U ("Kitty") protocols.
pub fn get_kitty_keyboard_capability() -> Capability {
let cap = KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed);
match cap {
x if x == Capability::Supported as _ => Capability::Supported,
x if x == Capability::NotSupported as _ => Capability::NotSupported,
_ => Capability::Unknown,
}
}
// Set CSI-U ("Kitty") support capability.
// This correctly handles the case where we think protocols are already enabled.
pub fn set_kitty_keyboard_capability(on_write: fn(), cap: Capability) {
assert_is_main_thread();
// Disable and renable protocols around capabilities.
let mut tty = TtyHandoff::new(on_write);
tty.disable_tty_protocols();
KITTY_KEYBOARD_SUPPORTED.store(cap as _, Ordering::Relaxed);
FLOG!(
term_protocols,
"Set Kitty keyboard capability to",
format!("{:?}", cap)
);
tty.reclaim();
}
// Helper to determine which keyboard protocols to enable.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ProtocolKind {
CSI_U, // Kitty keyboard support with CSI-U
Other, // Other protocols (e.g., modifyOtherKeys)
None, // No protocols
}
// Commands to emit to enable or disable TTY protocols. Each of these contains
// the full serialized command sequence as bytes. It's structured in this awkward
// way so that we can use it from a signal handler - no need to allocate or deallocate
// as Kitty support is discovered through tty queries.
struct ProtocolBytes {
csi_u: Box<[u8]>,
other: Box<[u8]>,
none: Box<[u8]>,
}
// The combined set of TTY protocols.
// This is created once at startup and then leaked, so it may be used
// from the SIGTERM handler.
struct TtyProtocolsSet {
// TTY metadata.
md: TtyMetadata,
// Variants to enable or disable tty protocols.
enablers: ProtocolBytes,
disablers: ProtocolBytes,
}
impl TtyProtocolsSet {
// Get commands to enable or disable TTY protocols, based on the metadata
// and the KITTY_KEYBOARD_SUPPORTED global variable.
// THIS IS USED FROM A SIGNAL HANDLER.
pub fn safe_get_commands(&self, enable: bool) -> &[u8] {
let protocol = self.md.safe_get_supported_protocol();
let cmds = if enable {
&self.enablers
} else {
&self.disablers
};
match protocol {
ProtocolKind::CSI_U => &cmds.csi_u,
ProtocolKind::Other => &cmds.other,
ProtocolKind::None => &cmds.none,
}
}
}
// Serialize a sequence of terminal commands into a byte array.
fn serialize_commands<'a>(cmds: impl Iterator<Item = TerminalCommand<'a>>) -> Box<[u8]> {
let mut out = Outputter::new_buffering();
for cmd in cmds {
out.write_command(cmd);
}
out.contents().into()
}
impl TtyMetadata {
// Determine which keyboard protocol to use based on the metadata
// and the KITTY_KEYBOARD_SUPPORTED global variable.
// This is used from a signal handler.
fn safe_get_supported_protocol(&self) -> ProtocolKind {
if self.pre_kitty_iterm2 {
return ProtocolKind::Other;
}
let cap = KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed);
match cap {
x if x == Capability::Supported as _ => ProtocolKind::CSI_U,
x if x == Capability::NotSupported as _ => ProtocolKind::Other,
_ => ProtocolKind::None,
}
}
// Return the protocols set to enable or disable TTY protocols.
fn get_protocols(self) -> TtyProtocolsSet {
// Enable focus reporting under tmux
let focus_reporting_on = || self.in_tmux.then_some(DecsetFocusReporting).into_iter();
let focus_reporting_off = || self.in_tmux.then_some(DecrstFocusReporting).into_iter();
let maybe_enable_focus_reporting = |protocols: &'static [TerminalCommand<'static>]| {
protocols.iter().cloned().chain(focus_reporting_on())
};
let maybe_disable_focus_reporting = |protocols: &'static [TerminalCommand<'static>]| {
protocols.iter().cloned().chain(focus_reporting_off())
};
let enablers = ProtocolBytes {
csi_u: serialize_commands(maybe_enable_focus_reporting(&[
DecsetBracketedPaste, // Enable bracketed paste
KittyKeyboardProgressiveEnhancementsEnable, // Kitty keyboard progressive enhancements
])),
other: serialize_commands(maybe_enable_focus_reporting(&[
DecsetBracketedPaste,
ModifyOtherKeysEnable, // XTerm's modifyOtherKeys
ApplicationKeypadModeEnable, // set application keypad mode, so the keypad keys send unique codes
])),
none: serialize_commands(maybe_enable_focus_reporting(&[DecsetBracketedPaste])),
};
let disablers = ProtocolBytes {
csi_u: serialize_commands(maybe_disable_focus_reporting(&[
DecrstBracketedPaste, // Disable bracketed paste
KittyKeyboardProgressiveEnhancementsDisable, // Kitty keyboard progressive enhancements
])),
other: serialize_commands(maybe_disable_focus_reporting(&[
DecrstBracketedPaste,
ModifyOtherKeysDisable,
ApplicationKeypadModeDisable,
])),
none: serialize_commands(maybe_disable_focus_reporting(&[DecrstBracketedPaste])),
};
TtyProtocolsSet {
md: self,
enablers,
disablers,
}
}
}
// The global tty protocols. This is set once at startup and not changed thereafter.
// This is an AtomicPtr and not a OnceLock, etc. so that it can be used from a signal handler.
static TTY_PROTOCOLS: AtomicPtr<TtyProtocolsSet> = AtomicPtr::new(std::ptr::null_mut());
// Get the TTY protocols, without initializing it.
fn tty_protocols() -> Option<&'static TtyProtocolsSet> {
// Safety: TTY_PROTOCOLS is never modified after initialization.
unsafe { TTY_PROTOCOLS.load(Ordering::Acquire).as_ref() }
}
// Get the TTY protocols, initializing it if necessary.
// This also initializes the terminal enable and disable serialized commands.
// Note in practice this is only used from the main thread - races are very unlikely.
fn get_or_init_tty_protocols() -> &'static TtyProtocolsSet {
use std::sync::atomic::Ordering::{Acquire, Release};
// Standard lazy-init pattern from rust-atomics-and-locks.
let mut p = TTY_PROTOCOLS.load(Acquire);
if p.is_null() {
// Try to swap in a new TTY protocols set.
p = Box::into_raw(Box::new(TtyMetadata::detect().get_protocols()));
if let Err(e) = TTY_PROTOCOLS.compare_exchange(std::ptr::null_mut(), p, Release, Acquire) {
// Safety: p comes from Box::into_raw right above,
// and wasn't shared with any other thread.
drop(unsafe { Box::from_raw(p) });
p = e;
}
}
// Safety: p is not null and points to a properly initialized value.
unsafe { &*p }
}
// Get the TTY metadata, initializing it if necessary.
pub fn tty_metadata() -> TtyMetadata {
get_or_init_tty_protocols().md
}
// Cover to merely initialize the TTY metadata, for clarity at call sites.
pub fn initialize_tty_metadata() {
tty_metadata();
}
// A marker of the current state of the tty protocols.
static TTY_PROTOCOLS_ACTIVE: AtomicBool = AtomicBool::new(false);
// A marker that the tty has been closed (SIGHUP, etc) and so we should not try to write to it.
static TTY_INVALID: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
// Enable or disable TTY protocols by writing the appropriate commands to the tty.
// Return true if we emitted any bytes to the tty.
// Note this does NOT intialize the TTY protocls if not already initialized.
fn set_tty_protocols_active(on_write: fn(), enable: bool) -> bool {
assert_is_main_thread();
// Have protocols at all? We require someone else to have initialized them.
let Some(protocols) = tty_protocols() else {
return false;
};
// Already set?
// Note we don't need atomic swaps as this is only called on the main thread.
// Also note we (logically) set and clear this even if we got SIGHUP.
if TTY_PROTOCOLS_ACTIVE.load(Ordering::Relaxed) == enable {
return false;
}
if enable {
TTY_PROTOCOLS_ACTIVE.store(true, Ordering::Release);
}
// Did we get SIGHUP?
if TTY_INVALID.load() {
return false;
}
// Write the commands to the tty, ignoring errors.
let commands = protocols.safe_get_commands(enable);
let _ = common::write_loop(&libc::STDOUT_FILENO, commands);
if !enable {
TTY_PROTOCOLS_ACTIVE.store(false, Ordering::Relaxed);
}
// Flog any terminal protocol changes of interest.
let mode = if enable { "Enabling" } else { "Disabling" };
match protocols.md.safe_get_supported_protocol() {
ProtocolKind::CSI_U => FLOG!(term_protocols, mode, "CSI-U extended keys"),
ProtocolKind::Other => FLOG!(term_protocols, mode, "other extended keys"),
ProtocolKind::None => (),
};
(on_write)();
true
}
// Helper to check if TTY protocols are active.
pub fn get_tty_protocols_active() -> bool {
TTY_PROTOCOLS_ACTIVE.load(Ordering::Relaxed)
}
// Called from a signal handler to deactivate TTY protocols before exiting.
// Only async-signal-safe code can be run here.
pub fn safe_deactivate_tty_protocols() {
// Safety: TTY_PROTOCOLS is never modified after initialization.
let protocols = unsafe { TTY_PROTOCOLS.load(Ordering::Acquire).as_ref() };
let Some(protocols) = protocols else {
// No protocols set, nothing to do.
return;
};
if !TTY_PROTOCOLS_ACTIVE.load(Ordering::Acquire) {
return;
}
// Did we get SIGHUP?
if TTY_INVALID.load() {
return;
}
let commands = protocols.safe_get_commands(false);
// Safety: just writing data to stdout.
let _ = safe_write_loop(&libc::STDOUT_FILENO, commands);
TTY_PROTOCOLS_ACTIVE.store(false, Ordering::Release);
}
// Called from a signal handler to mark the tty as invalid (e.g. SIGHUP).
// This suppresses any further attempts to write protocols to the tty,
pub fn safe_mark_tty_invalid() {
TTY_INVALID.store(true);
}
// Allows transferring the tty to a job group, while it runs, in a scoped fashion.
// This has several responsibilities:
// - Invoking tcsetpgrp() to transfer the tty to the job group.
// Note this is complex because it is inherently "racey."
// - Saving tty modes if a job stops. That is, if a job is running and
// then it stops in the background, we want to record the tty modes
// it has in the job, so that we can restore them when the job is resumed.
// - Managing enabling and disabling terminal protocols (bracketed paste, etc).
// Note it only ever makes sense to run this on the main thread.
pub struct TtyHandoff {
// The job group which owns the tty, or empty if none.
owner: Option<JobGroupRef>,
// Whether terminal protocols were initially enabled.
// reclaim() restores the state to this.
tty_protocols_initial: bool,
// The state of terminal protocols that we set.
// Note we track this separately from TTY_PROTOCOLS_ACTIVE. We undo the changes
// we make.
tty_protocols_applied: bool,
// Whether reclaim was called, restoring the tty to its pre-scoped value.
reclaimed: bool,
// Called after writing to the TTY.
on_write: fn(),
}
impl TtyHandoff {
pub fn new(on_write: fn()) -> Self {
let protocols_active = get_tty_protocols_active();
TtyHandoff {
owner: None,
tty_protocols_initial: protocols_active,
tty_protocols_applied: protocols_active,
reclaimed: false,
on_write,
}
}
/// Mark terminal modes as enabled.
/// Return true if something was written to the tty.
pub fn enable_tty_protocols(&mut self) -> bool {
if self.tty_protocols_applied {
return false; // Already enabled.
}
self.tty_protocols_applied = true;
set_tty_protocols_active(self.on_write, true)
}
/// Mark terminal modes as disabled.
/// Return true if something was written to the tty.
pub fn disable_tty_protocols(&mut self) -> bool {
if !self.tty_protocols_applied {
return false; // Already disabled.
};
self.tty_protocols_applied = false;
set_tty_protocols_active(self.on_write, false)
}
/// Transfer to the given job group, if it wants to own the terminal.
#[allow(clippy::wrong_self_convention)]
pub fn to_job_group(&mut self, jg: &JobGroupRef) {
assert!(self.owner.is_none(), "Terminal already transferred");
if Self::try_transfer(jg) {
self.owner = Some(jg.clone());
}
}
/// Reclaim the tty if we transferred it.
/// Returns true if data was written to the tty, as part of
/// re-enabling terminal protocols.
pub fn reclaim(mut self) -> bool {
self.reclaim_impl()
}
/// Release the tty, meaning no longer restore anything in Drop - similar to `mem::forget`.
pub fn release(mut self) {
self.reclaimed = true;
}
/// Implementation of reclaim, factored out for use in Drop.
fn reclaim_impl(&mut self) -> bool {
assert!(!self.reclaimed, "Terminal already reclaimed");
self.reclaimed = true;
if self.owner.is_some() {
FLOG!(proc_pgroup, "fish reclaiming terminal");
if unsafe { libc::tcsetpgrp(STDIN_FILENO, libc::getpgrp()) } == -1 {
FLOG!(warning, "Could not return shell to foreground");
perror("tcsetpgrp");
}
self.owner = None;
}
// Restore the terminal protocols. Note this does nothing if they were unchanged.
if self.tty_protocols_initial {
self.enable_tty_protocols()
} else {
self.disable_tty_protocols()
}
}
/// Save the current tty modes into the owning job group, if we are transferred.
pub fn save_tty_modes(&mut self) {
if let Some(ref mut owner) = self.owner {
let mut tmodes = MaybeUninit::uninit();
if unsafe { libc::tcgetattr(STDIN_FILENO, tmodes.as_mut_ptr()) } == 0 {
owner.tmodes.replace(Some(unsafe { tmodes.assume_init() }));
} else if errno::errno().0 != ENOTTY {
perror("tcgetattr");
}
}
}
fn try_transfer(jg: &JobGroup) -> bool {
if !jg.wants_terminal() {
// The job doesn't want the terminal.
return false;
}
// Get the pgid; we must have one if we want the terminal.
let pgid = jg.get_pgid().unwrap();
// It should never be fish's pgroup.
let fish_pgrp = crate::nix::getpgrp();
assert!(
pgid.as_pid_t() != fish_pgrp,
"Job should not have fish's pgroup"
);
// Ok, we want to transfer to the child.
// Note it is important to be very careful about calling tcsetpgrp()!
// fish ignores SIGTTOU which means that it has the power to reassign the tty even if it doesn't
// own it. This means that other processes may get SIGTTOU and become zombies.
// Check who own the tty now. There's four cases of interest:
// 1. There is no tty at all (tcgetpgrp() returns -1). For example running from a pure script.
// Of course do not transfer it in that case.
// 2. The tty is owned by the process. This comes about often, as the process will call
// tcsetpgrp() on itself between fork and exec. This is the essential race inherent in
// tcsetpgrp(). In this case we want to reclaim the tty, but do not need to transfer it
// ourselves since the child won the race.
// 3. The tty is owned by a different process. This may come about if fish is running in the
// background with job control enabled. Do not transfer it.
// 4. The tty is owned by fish. In that case we want to transfer the pgid.
let current_owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
if current_owner < 0 {
// Case 1.
return false;
} else if current_owner == pgid.get() {
// Case 2.
return true;
} else if current_owner != pgid.get() && current_owner != fish_pgrp {
// Case 3.
return false;
}
// Case 4 - we do want to transfer it.
// The tcsetpgrp(2) man page says that EPERM is thrown if "pgrp has a supported value, but
// is not the process group ID of a process in the same session as the calling process."
// Since we _guarantee_ that this isn't the case (the child calls setpgid before it calls
// SIGSTOP, and the child was created in the same session as us), it seems that EPERM is
// being thrown because of an caching issue - the call to tcsetpgrp isn't seeing the
// newly-created process group just yet. On this developer's test machine (WSL running Linux
// 4.4.0), EPERM does indeed disappear on retry. The important thing is that we can
// guarantee the process isn't going to exit while we wait (which would cause us to possibly
// block indefinitely).
while unsafe { libc::tcsetpgrp(STDIN_FILENO, pgid.as_pid_t()) } != 0 {
FLOGF!(proc_termowner, "tcsetpgrp failed: %d", errno::errno().0);
// Before anything else, make sure that it's even necessary to call tcsetpgrp.
// Since it usually _is_ necessary, we only check in case it fails so as to avoid the
// unnecessary syscall and associated context switch, which profiling has shown to have
// a significant cost when running process groups in quick succession.
let getpgrp_res = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
if getpgrp_res < 0 {
match errno::errno().0 {
ENOTTY => {
// stdin is not a tty. This may come about if job control is enabled but we are
// not a tty - see #6573.
return false;
}
_ => {
perror("tcgetpgrp");
return false;
}
}
}
if getpgrp_res == pgid.get() {
FLOGF!(
proc_termowner,
"Process group %d already has control of terminal",
pgid
);
return true;
}
let pgroup_terminated;
if errno::errno().0 == EINVAL {
// OS X returns EINVAL if the process group no longer lives. Probably other OSes,
// too. Unlike EPERM below, EINVAL can only happen if the process group has
// terminated.
pgroup_terminated = true;
} else if errno::errno().0 == EPERM {
// Retry so long as this isn't because the process group is dead.
let mut result: libc::c_int = 0;
let wait_result = unsafe { libc::waitpid(-pgid.as_pid_t(), &mut result, WNOHANG) };
if wait_result == -1 {
// Note that -1 is technically an "error" for waitpid in the sense that an
// invalid argument was specified because no such process group exists any
// longer. This is the observed behavior on Linux 4.4.0. a "success" result
// would mean processes from the group still exist but is still running in some
// state or the other.
pgroup_terminated = true;
} else {
// Debug the original tcsetpgrp error (not the waitpid errno) to the log, and
// then retry until not EPERM or the process group has exited.
FLOGF!(
proc_termowner,
"terminal_give_to_job(): EPERM with pgid %d.",
pgid
);
continue;
}
} else if errno::errno().0 == ENOTTY {
// stdin is not a TTY. In general we expect this to be caught via the tcgetpgrp
// call's EBADF handler above.
return false;
} else {
FLOGF!(
warning,
"Could not send job %d ('%ls') with pgid %d to foreground",
jg.job_id.to_wstring(),
jg.command,
pgid
);
perror("tcsetpgrp");
return false;
}
if pgroup_terminated {
// All processes in the process group has exited.
// Since we delay reaping any processes in a process group until all members of that
// job/group have been started, the only way this can happen is if the very last
// process in the group terminated and didn't need to access the terminal, otherwise
// it would have hung waiting for terminal IO (SIGTTIN). We can safely ignore this.
FLOGF!(
proc_termowner,
"tcsetpgrp called but process group %d has terminated.\n",
pgid
);
return false;
}
break;
}
true
}
}
/// The destructor will assert if reclaim() has not been called.
impl Drop for TtyHandoff {
fn drop(&mut self) {
if !self.reclaimed {
self.reclaim_impl();
}
}
}
// If we are running under iTerm2, get the version as a tuple of (major, minor, patch).
fn get_iterm2_version() -> Option<(u32, u32, u32)> {
use std::env::var;
let term = var("LC_TERMINAL").ok()?;
if term != "iTerm2" {
return None;
}
let version = var("LC_TERMINAL_VERSION").ok()?;
let mut parts = version.split('.');
Some((
parts.next()?.parse().ok()?,
parts.next()?.parse().ok()?,
parts.next()?.parse().ok()?,
))
}

View File

@@ -143,11 +143,6 @@ echo -e 'abc\121def'
echo -e 'abc\1212def'
#CHECK: abcQdef
#CHECK: abcQ2def
# Test octal overflow: \5555 = 555 octal = 365 decimal, wraps to 109 decimal (155 octal)
# Followed by literal '5' character (065 octal)
echo -ne '\5555' | display_bytes
#CHECK: 0000000 155 065
#CHECK: 0000002
echo -e 'abc\cdef' # won't output a newline!
#CHECK: abc
echo ''

View File

@@ -1,7 +1,5 @@
#RUN: %fish %s
set -g fish (status fish-path)
commandline --input "echo foo | bar" --is-valid
and echo Valid
# CHECK: Valid
@@ -52,16 +50,7 @@ commandline --input "echo > {a,b}" --tokens-expanded
commandline --input "echo {arg1,arg2} <in >out" --tokens-raw
# CHECK: echo
# CHECK: {arg1,arg2}
$fish -ic '
commandline hello
commandline
commandline -i world
commandline
commandline --cursor 5
commandline -i " "
commandline
'
# CHECK: hello
# CHECK: helloworld
# CHECK: hello world
# CHECK: <
# CHECK: in
# CHECK: >
# CHECK: out

View File

@@ -423,6 +423,38 @@ set -S var
# CHECK: $var[1]: |1|
# CHECK: $var[2]: |}|
# Raw tokens into named variables
echo 'echo "&" a\ b &
second line (dropped)' | read -l --tokenize-raw head tail
set -S head tail
# CHECK: $head: set in local scope, unexported, with 1 elements
# CHECK: $head[1]: |echo|
# CHECK: $tail: set in local scope, unexported, with 1 elements
# CHECK: $tail[1]: |"&" a\\ b &|
# Raw tokens into list
echo 'echo "&" & a\ b
second line (dropped)' | read -l --tokenize-raw -a rawlist
set -S rawlist
# CHECK: $rawlist: set in local scope, unexported, with 4 elements
# CHECK: $rawlist[1]: |echo|
# CHECK: $rawlist[2]: |"&"|
# CHECK: $rawlist[3]: |&|
# CHECK: $rawlist[4]: |a\\ b|
echo 'echo "&" & a\ b
second line' | read -l --tokenize-raw -a rawlist_null -z
set -S rawlist_null
# CHECK: $rawlist_null: set in local scope, unexported, with 8 elements
# CHECK: $rawlist_null[1]: |echo|
# CHECK: $rawlist_null[2]: |"&"|
# CHECK: $rawlist_null[3]: |&|
# CHECK: $rawlist_null[4]: |a\\ b|
# CHECK: $rawlist_null[5]: |\n|
# CHECK: $rawlist_null[6]: |second|
# CHECK: $rawlist_null[7]: |line|
# CHECK: $rawlist_null[8]: |\n|
echo '1 {} "{}"' | read -lat var
echo $var
# CHECK: 1 {} {}

View File

@@ -1,115 +0,0 @@
# RUN: %fish --interactive %s
fish_vi_key_bindings
commandline '1'; commandline --cursor 0; fish_vi_dec
commandline --current-buffer
# CHECK: 0
commandline '0'; commandline --cursor 0; fish_vi_dec
commandline --current-buffer
# CHECK: -1
commandline -- '-1'; commandline --cursor 0; fish_vi_dec
commandline --current-buffer
# CHECK: -2
commandline -- '-1'; commandline --cursor 0; fish_vi_inc
commandline --current-buffer
# CHECK: 0
commandline '0'; commandline --cursor 0; fish_vi_inc
commandline --current-buffer
# CHECK: 1
commandline '123'; commandline --cursor 0; fish_vi_inc
commandline --current-buffer
# CHECK: 124
commandline '123'; commandline --cursor 0; fish_vi_dec
commandline --current-buffer
# CHECK: 122
commandline '123'; commandline --cursor 1; fish_vi_inc
commandline --current-buffer
# CHECK: 124
commandline '123'; commandline --cursor 1; fish_vi_dec
commandline --current-buffer
# CHECK: 122
commandline '123'; commandline --cursor 2; fish_vi_inc
commandline --current-buffer
# CHECK: 124
commandline '123'; commandline --cursor 2; fish_vi_dec
commandline --current-buffer
# CHECK: 122
commandline 'abc123'; commandline --cursor 1; fish_vi_inc
commandline --current-buffer
# CHECK: abc124
commandline 'abc123'; commandline --cursor 1; fish_vi_dec
commandline --current-buffer
# CHECK: abc122
commandline 'abc123def'; commandline --cursor 1; fish_vi_inc
commandline --current-buffer
# CHECK: abc124def
commandline 'abc123def'; commandline --cursor 1; fish_vi_dec
commandline --current-buffer
# CHECK: abc122def
commandline 'abc123def'; commandline --cursor 5; fish_vi_inc
commandline --current-buffer
# CHECK: abc124def
commandline 'abc123def'; commandline --cursor 5; fish_vi_dec
commandline --current-buffer
# CHECK: abc122def
commandline 'abc123def'; commandline --cursor 6; fish_vi_inc
commandline --current-buffer
# CHECK: abc123def
commandline 'abc123def'; commandline --cursor 6; fish_vi_dec
commandline --current-buffer
# CHECK: abc123def
commandline 'abc99def'; commandline --cursor 1; fish_vi_inc
commandline --current-buffer
# CHECK: abc100def
commandline 'abc99def'; commandline --cursor 1; fish_vi_dec
commandline --current-buffer
# CHECK: abc98def
commandline 'abc-99def'; commandline --cursor 1; fish_vi_inc
commandline --current-buffer
# CHECK: abc-98def
commandline 'abc-99def'; commandline --cursor 1; fish_vi_dec
commandline --current-buffer
# CHECK: abc-100def
commandline '2022-04-09'; commandline --cursor 7; fish_vi_inc
commandline --current-buffer
# CHECK: 2022-04-08
commandline 'to 2022-04-09'; commandline --cursor 4; fish_vi_inc
commandline --current-buffer
# CHECK: to 2023-04-09
commandline 'to 2022-04-09'; commandline --cursor 4; fish_vi_dec
commandline --current-buffer
# CHECK: to 2021-04-09
commandline 'to 2022-04-09'; commandline --cursor 11; fish_vi_dec
commandline --current-buffer
# CHECK: to 2022-04-10
commandline 'to 2022-04-09'; commandline --cursor 11; fish_vi_inc
commandline --current-buffer
# CHECK: to 2022-04-08

View File

@@ -102,18 +102,6 @@ send(control("k"))
sendline('echo "process extent is [$tmp]"')
expect_str("process extent is [echo process # comment]")
sendline(
"""$fish -C 'commandline "sq 2; exit"; commandline --cursor 1; commandline -i e'"""
)
expect_str("seq 2")
send("\r")
expect_str("1\r\n2\r\n")
sendline("""$fish -C 'commandline 123; read'""")
expect_str("read> 123")
sendline("456; exit")
expect_str("123456")
# DISABLED because it keeps failing under ASAN
# sendline(r"bind ctrl-b 'set tmp (commandline --current-process | count)'")
# sendline(r'commandline "echo line1 \\" "# comment" "line2"')

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env python3
import argparse
import asyncio
from dataclasses import dataclass
from datetime import datetime
import os
from pathlib import Path
import resource
import shutil
import subprocess
import sys
@@ -205,33 +205,25 @@ async def main():
if semaphore is not None:
semaphore.release()
tasks = [asyncio.create_task(run(f, arg), name=arg) for f, arg in files]
while tasks:
done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
for task in done:
try:
result = await task
except Exception as e:
arg = task.get_name()
result = TestFail(
arg, None, f"Test '{arg}' raised an exception: {e}"
)
# TODO(python>3.8): use match statement
if isinstance(result, TestSkip):
arg = result.arg
skipcount += 1
print_result(arg, "SKIPPED", BLUE)
elif isinstance(result, TestFail):
# fmt: off
arg, duration_ms, error_message = result.arg, result.duration_ms, result.error_message
# fmt: on
failcount += 1
failed += [arg]
print_result(arg, "FAILED", RED, duration_ms, error_message)
elif isinstance(result, TestPass):
arg, duration_ms = result.arg, result.duration_ms
passcount += 1
print_result(arg, "PASSED", GREEN, duration_ms)
tasks = [run(f, arg) for f, arg in files]
for task in asyncio.as_completed(tasks):
result = await task
# TODO(python>3.8): use match statement
if isinstance(result, TestSkip):
arg = result.arg
skipcount += 1
print_result(arg, "SKIPPED", BLUE)
elif isinstance(result, TestFail):
# fmt: off
arg, duration_ms, error_message = result.arg, result.duration_ms, result.error_message
# fmt: on
failcount += 1
failed += [arg]
print_result(arg, "FAILED", RED, duration_ms, error_message)
elif isinstance(result, TestPass):
arg, duration_ms = result.arg, result.duration_ms
passcount += 1
print_result(arg, "PASSED", GREEN, duration_ms)
if passcount + failcount + skipcount > 1:
print(f"{passcount} / {passcount + failcount} passed ({skipcount} skipped)")
@@ -243,35 +235,23 @@ async def main():
return 1 if failcount else 0
# TODO(python>=3.7): @dataclass
@dataclass
class TestSkip:
arg: str
def __init__(self, arg: str):
self.arg = arg
@dataclass
class TestFail:
arg: str
duration_ms: Optional[int]
error_message: Optional[str]
def __init__(
self, arg: str, duration_ms: Optional[int], error_message: Optional[str]
):
self.arg = arg
self.duration_ms = duration_ms
self.error_message = error_message
@dataclass
class TestPass:
arg: str
duration_ms: int
def __init__(self, arg: str, duration_ms: Optional[int]):
self.arg = arg
self.duration_ms = duration_ms
TestResult = Union[TestSkip, TestFail, TestPass]
@@ -354,27 +334,9 @@ async def run_test(
return TestFail(arg, None, "Error in test driver. This should be unreachable.")
if sys.version_info < (3, 7):
def asyncio_run(coro):
loop = asyncio.get_event_loop()
try:
return loop.run_until_complete(coro)
finally:
if not loop.is_closed():
loop.close()
else:
asyncio_run = asyncio.run
if __name__ == "__main__":
# Increase the maximum number of open files to at least 4096,
# as we run tests concurrently.
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
if soft < 4096:
resource.setrlimit(resource.RLIMIT_NOFILE, (min(4096, hard), hard))
try:
ret = asyncio_run(main())
ret = asyncio.run(main())
sys.exit(ret)
except KeyboardInterrupt:
sys.exit(130)