Compare commits

..

36 Commits

Author SHA1 Message Date
David Adam
08cd59727b CI: use build_tools/check.sh in Cirrus CI
08b03a733a removed CMake from the Docker images used for the
Cirrus builds.

It might be better to use fish_run_tests.sh in the Docker image, but
that requires some context which I'm not sure is set up properly in
Cirrus.
2025-10-05 23:25:04 +08:00
David Adam
8e5046061d build_tools/check.sh: add support for FISH_TEST_MAX_CONCURRENCY 2025-10-05 23:00:11 +08:00
Peter Ammon
e9f5982147 Fix a clipply 2025-10-04 19:25:10 -07:00
Peter Ammon
50819666b1 Remove our own C bits
fish-shell itself no longer depends on a C compiler; however we still
use cc for feature detection. Removing that will have to wait for another day.
2025-10-04 18:56:11 -07:00
Peter Ammon
6ad13e35c0 Bravely define PATH_BSHELL
PATH_BSHELL is always "/bin/sh" except on Android where it's "/system/bin/sh".

This isn't exposed by Rust, so just define it ourselves.
2025-10-04 17:27:16 -07:00
Peter Ammon
39e2f1138b Bravely stop setting stdout to unbuffered
Issue #3748 made stdout (the C FILE*, NOT the file descriptor) unbuffered,
due to concerns about mixing output to the stdout FILE* with output output.

We no longer write to the C FILE* and Rust libc doesn't expose stdout, which may
be a macro. This code no longer looks useful. Bravely remove it.
2025-10-04 17:27:15 -07:00
Peter Ammon
cd37c71e29 Adopt Rust libc RLIMIT_* fields
Moving more stuff out of C.
2025-10-04 14:09:47 -07:00
Peter Ammon
c1f3d93b3b Adopt Rust libc::MNT_LOCAL
Note the ST_LOCAL usage on NetBSD is also covered by this.
2025-10-04 14:09:46 -07:00
Peter Ammon
0aa05032c4 Adopt rust _PC_CASE_SENSITIVE
fish no longer needs to expose this - the libc crate does the job.
2025-10-04 14:01:13 -07:00
Peter Ammon
174130fe2f Adopt Rust libc _CS_PATH
This is now supported directly by the libc crate - no need for fish
to expose this via C.
2025-10-04 14:01:11 -07:00
Peter Ammon
d06f7f01d2 Remove MB_CUR_MAX from our libc ffi
We no longer need this.
2025-10-04 13:39:22 -07:00
Peter Ammon
a04ddd9b17 Adopt get_is_multibyte_locale in the pager 2025-10-04 13:39:22 -07:00
Peter Ammon
12929fed74 Adopt get_is_multibyte_locale in decode_input_byte
Move away from MB_CUR_MAX
2025-10-04 13:39:22 -07:00
Peter Ammon
87bf580f68 Adopt get_is_multibyte_locale in wcs2string_callback
Move away from MB_CUR_MAX
2025-10-04 13:39:22 -07:00
Peter Ammon
66bab5e767 Early work aiming to remove MB_CUR_MAX from fish libc FFI
Start detecting multibyte locales in Rust.
2025-10-04 13:39:21 -07:00
Peter Ammon
4b12fb2887 Migrate invalidate_numeric_locale into fish_setlocale
Centralizes where locale information is recomputed.
2025-10-04 13:39:21 -07:00
Johannes Altmanninger
623c14aed0 Kitty keyboard protocol is non-functional on old versions of Zellij
try_readb() uses a high timeout when the kitty keyboard protocol is
enabled, because in that case it should basically never be necessary
to interpret \e as escape key, see 30ff3710a0 (Increase timeout when
reading escape sequences inside paste/kitty kbd, 2025-07-24).

Zellij before commit 0075548a (fix(terminal): support kitty keyboard
protocol setting with "=" (#3942), 2025-01-17) fails to enable kitty
keyboard protocol, so it sends the raw escape bytes, causing us to
wait 300ms.

Closes #11868
2025-10-04 07:17:34 +02:00
Johannes Altmanninger
7d83dc4758 Refresh TTY timestamps after firing focus events
Using a multi-line prompt with focus events on:

	tmux new-session fish -C '
		tmux set -g focus-events on
		set -g fish_key_bindings fish_vi_key_bindings
		function fish_prompt
		    echo (prompt_pwd)
		    echo -n "> "
		end
		tmux split
	'

switching to the fish pane and typing any key sometimes leads to our
two-line-prompt being redawn one line below it's actual place.

Reportedly, it bisects to d27f5a5 which changed when we print things.
I did not verify root cause, but
1. symptoms are very similar to other
   problems with TTY timestamps, see eaa837effa (Refresh TTY
   timestamps again in most cases, 2025-07-24).
2. this seems fixed if we refresh timestamps after
   running the focus events, which print some cursor shaping commands
   to stdout. So bravely do that.

Closes #11870
2025-10-03 22:35:31 +02:00
Johannes Altmanninger
493d0bca95 Update changelog for patch release 2025-10-03 22:01:39 +02:00
qianlongzt
983501ff8c zh_CN: fix vi case
Part of #11854
2025-10-03 20:51:57 +02:00
The0x539
20da9a2b51 ast: use macro_rules_attribute for the Acceptor trait
Closes #11867
2025-10-03 20:45:01 +02:00
The0x539
7aec6c55f9 ast: use macro_rules_attribute for Leaf trait
Part of #11867
2025-10-03 20:45:01 +02:00
The0x539
532f30e031 ast: use macro_rules_attribute for Node trait
Part of #11867
2025-10-03 20:45:01 +02:00
Daniel Rainer
1d7ab57e3a xgettext: remove --strict flag from msguniq
As with `msgmerge`, this introduces unwanted empty comment lines above
`#, c-format`
lines. We don't need this strict formatting, so we get rid of the flag
and the associated empty comment lines.

Closes #11863
2025-10-03 20:22:59 +02:00
Étienne Deparis
8adc598e90 web_config: Support long options separated with = from their value
Closes #11861
2025-10-03 20:18:38 +02:00
Étienne Deparis
c884c08257 web_config: Use None as default for underline style
Underline is no more a boolean and should be one of the accepted style,
or None. By keeping False as default value, web_config was generating
wrong --underline=False settings

Part of #11861
2025-10-03 20:18:38 +02:00
Daniel Rainer
66dc734c11 printf: remove useless length modifiers
Closes #11858
2025-10-03 20:14:20 +02:00
Daniel Rainer
77fee9acb9 printf: rename direc -> directive
The abbreviation is ambiguous, which makes the code unnecessarily hard
to read. (possible misleading expansions: direct, direction, director,
...)

Part of #11858
2025-10-03 20:14:20 +02:00
Daniel Rainer
6b66c2bc1d printf: use options for idiomatic code
The `have_foo: bool` + `foo: i64` combination is more idiomatically
represented as `foo: Option<i64>`. This change is applied for
`field_width` and `precision`.

In addition, the sketchy explicit cast from `i64` to `c_int`, and the
subsequent implicit cast from `c_int` to `i32` are avoided by using
`i64` consistently.

Part of #11858
2025-10-03 20:14:20 +02:00
Daniel Rainer
81b9f50dc2 printf: reformat doc comments
Part of #11858
2025-10-03 20:14:20 +02:00
Johannes Altmanninger
fcd246064b Stop requesting modifyOtherKeys on old Midnight Commander again
This upstream issue was fixed in 0ea77d2ec (Ticket #4597: fix CSI
parser, 2024-10-09); for old mc's we had worked around this but the
workaround was accidentally removed. Add it back for all the versions
that don't have that fix.

Fixes f0e007c439 (Relocate tty metadata and protocols and clean
it up, 2025-06-19) Turns out this was why the "Capability" enum was
added in 2d234bb676 (Only request keyboard protocols once we know
if kitty kbd is supported, 2025-01-26).

Fixes #11869
2025-10-03 20:14:20 +02:00
Johannes Altmanninger
86a0a348ee Harmonize temporary Midnight Commander workarounds a bit 2025-10-03 18:18:05 +02:00
Johannes Altmanninger
ed36e852d2 release.sh: add next patch milestone
This is still the common case.
2025-10-03 18:08:28 +02:00
Johannes Altmanninger
da5d93c1e2 release.sh: close milestone when done
Don't fail early if this doesn't exist, because it's not needed for
testing this on fish-shell forks that live on GitHub.
2025-10-03 18:08:28 +02:00
Johannes Altmanninger
7b59ae0d82 Unbreak hack to strip " (deleted)" suffix from executable path
Commit 49b88868df (Fix stripping of " (deleted)" from non-UTF8 paths
to fish, 2024-10-12) was wrong because Path::ends_with() considers
entire path components. Fix that.

Refs:
- https://matrix.to/#/!YLTeaulxSDauOOxBoR:matrix.org/$k2IQazfmztFUXrairmIQvx_seS1ZJ7HlFWhmNy479Dg
- https://matrix.to/#/!YLTeaulxSDauOOxBoR:matrix.org/$4pugfHejL9J9L89zuFU6Bfg41UMjA0y79orc3EaBego
2025-10-03 18:08:28 +02:00
Fabian Boehm
97acc12d62 Fix scp completions
Introduced when __fish_mktemp_relative was.

Fixes #11860
2025-10-02 18:20:41 +02:00
43 changed files with 581 additions and 931 deletions

View File

@@ -25,10 +25,7 @@ linux_task:
# Unrestriced parallelism results in OOM
- lscpu || true
- (cat /proc/meminfo | grep MemTotal) || true
- mkdir build && cd build
- FISH_TEST_MAX_CONCURRENCY=6 cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ..
- ninja -j 6 fish
- ninja fish_run_tests
- FISH_TEST_MAX_CONCURRENCY=6 build_tools/check.sh
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'
linux_arm_task:
@@ -44,11 +41,7 @@ linux_arm_task:
# Unrestriced parallelism results in OOM
- lscpu || true
- (cat /proc/meminfo | grep MemTotal) || true
- mkdir build && cd build
- FISH_TEST_MAX_CONCURRENCY=6 cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ..
- ninja -j 6 fish
- file ./fish
- ninja fish_run_tests
- FISH_TEST_MAX_CONCURRENCY=6 build_tools/check.sh
# CI task disabled during RIIR transition
only_if: false && $CIRRUS_REPO_OWNER == 'fish-shell'
@@ -68,10 +61,7 @@ freebsd_task:
- pw user add -n fish-user -s /bin/csh -d /home/fish-user
- mkdir -p /home/fish-user
- chown -R fish-user /home/fish-user
- mkdir build && cd build
- chown -R fish-user ..
- chown -R fish-user .
- sudo -u fish-user -s whoami
- sudo -u fish-user -s FISH_TEST_MAX_CONCURRENCY=1 cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ..
- sudo -u fish-user -s ninja -j 6 fish
- sudo -u fish-user -s ninja fish_run_tests
- sudo -u fish-user -s FISH_TEST_MAX_CONCURRENCY=1 build_tools/check.sh
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'

View File

@@ -1,56 +0,0 @@
on:
pull_request_target:
types: [opened, synchronize]
workflow_dispatch:
inputs:
pr_number:
description: Pull request number
required: true
type: number
permissions:
contents: write
jobs:
update-event-feeds:
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: |
sudo apt install python3-git python3-polib
sudo pip3 install --break-system-packages feedgen==1.0.0
- name: Update event feeds
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -x
if [ "${{ github.event_name }}" = pull_request ]; then
pr_number=${{ github.event.pull_request.number }}
else
pr_number=${{ inputs.pr_number }}
fi
# E.g. https://github.com/fish-shell/fish-shell
repo_url=${{ github.server_url }}/${{ github.repository }}
workflow_sha=${{ github.workflow_sha }}
run_id=${{ github.run_id }}
events_ref=refs/fish-shell-events
git clone "$repo_url" --revision="$events_ref" worktree --depth=1
cd worktree
git fetch origin "$workflow_sha" --depth=1
set -o pipefail
git show "$workflow_sha":build_tools/event-feeds.py |
python - "$pr_number"
git add .
if git diff --cached --exit-code; then
exit
fi
git config user.name fish-shell-bot
git config user.email fish-shell-bot
git commit -m "update via github run ID $run_id"
cat >>.git/config <<'EOF'
[credential]
username = fish-shell-bot
helper = !sh -c 'echo "password=$GITHUB_TOKEN"'
EOF
# Might fail due to a race; that's usually fine to ignore...
git push origin HEAD:"$events_ref" ||:

View File

@@ -1,11 +1,26 @@
fish ?.?.? (released ???)
=========================
fish 4.1.1 (released ???)
fish 4.1.2 (released ???)
=========================
This release fixes the following regressions identified in 4.1.0:
- Fixed spurious error output when completing remote file paths for ``scp`` (:issue:`11860`).
- Fixed an issue where focus events (currently only enabled in ``tmux``) would cause multiline prompts to be redrawn in the wrong line (:issue:`11870`).
- Stopped printing output that would cause a glitch on old versions of Midnight Commander (:issue:`11869`).
- Added a workaround for old versions of Zellij where :kbd:`escape` processing was delayed (:issue:`11868`).
- Fixed a case where the :doc:`web-based configuration tool <cmds/fish_config>` would generate invalid configuration (:issue:`11861`).
- Fixed a case where upgrading fish would break old versions of fish that were still running.
In general, fish still needs to be restarted after it is upgraded,
except for `standalone builds <https://github.com/fish-shell/fish-shell/?tab=readme-ov-file#building-fish-with-embedded-data-experimental>`__.
fish 4.1.1 (released September 30, 2025)
========================================
This release fixes the following regressions identified in 4.1.0:
- Many of our new Chinese translations were more confusing than helpful; they have been fixed or removed (:issue:`11833`).
Note that you can work around this type of issue by configuring fish's :doc:`message localization <cmds/_>`:

23
Cargo.lock generated
View File

@@ -118,6 +118,7 @@ dependencies = [
"fish-printf",
"libc",
"lru",
"macro_rules_attribute",
"nix",
"num-traits",
"once_cell",
@@ -254,6 +255,22 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "macro_rules_attribute"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520"
dependencies = [
"macro_rules_attribute-proc_macro",
"paste",
]
[[package]]
name = "macro_rules_attribute-proc_macro"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
[[package]]
name = "memchr"
version = "2.7.4"
@@ -326,6 +343,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pcre2"
version = "0.2.9"

View File

@@ -84,6 +84,7 @@ fish-gettext-maps = { workspace = true, optional = true }
fish-printf.workspace = true
libc.workspace = true
lru.workspace = true
macro_rules_attribute = "0.2.2"
nix.workspace = true
num-traits.workspace = true
once_cell.workspace = true

View File

@@ -50,9 +50,6 @@ fn main() {
#[cfg(feature = "gettext-extract")]
rsconf::rebuild_if_env_changed("FISH_GETTEXT_EXTRACTION_FILE");
rsconf::rebuild_if_path_changed("src/libc.c");
cc::Build::new().file("src/libc.c").compile("flibc.a");
let build = cc::Build::new();
let mut target = Target::new_from(build).unwrap();
// Keep verbose mode on until we've ironed out rust build script stuff
@@ -172,10 +169,7 @@ fn has_small_stack(_: &Target) -> bool {
// Modern macOS versions default to an 8 MiB main stack but legacy OS X have a 0.5 MiB one.
let stack_size = unsafe { pthread_get_stacksize_np(pthread_self()) };
const TWO_MIB: usize = 2 * 1024 * 1024 - 1;
match stack_size {
0..=TWO_MIB => true,
_ => false,
}
stack_size <= TWO_MIB
}
}

View File

@@ -43,6 +43,11 @@ fi
# Currently, all builds are debug builds.
build_dir="$target_dir/debug"
if [ -n "$FISH_TEST_MAX_CONCURRENCY" ]; then
export RUST_TEST_THREADS="$FISH_TEST_MAX_CONCURRENCY"
export CARGO_BUILD_JOBS="$FISH_TEST_MAX_CONCURRENCY"
fi
template_file=$(mktemp)
FISH_GETTEXT_EXTRACTION_FILE=$template_file cargo build --workspace --all-targets --features=gettext-extract
if $lint; then

View File

@@ -1,155 +0,0 @@
from datetime import datetime
from pathlib import Path
from typing import Tuple
import dateutil.tz
import json
import re
import os
import subprocess
import sys
import xml.etree.ElementTree as ET
# TODO maybe use https://github.com/getpelican/feedgenerator instead?
from feedgen.feed import FeedGenerator
import git
import polib
def message_dictionary(repo, ref, file: Path):
po_contents = repo.git().show(f"{ref}:{file}")
messages = polib.pofile(po_contents)
return {e.msgid: e.msgstr for e in messages}
def write_feed(
feed_path: Path,
base_ref_oid: str,
title: str,
subtitle: str,
this_pr: Tuple[str, str] | None,
):
fg = FeedGenerator()
tree_url = (
"https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events"
)
feed_url = f"{tree_url}/{feed_path}"
fg.id(feed_url)
fg.title(title)
fg.subtitle(subtitle)
fg.link(href=feed_url, rel="self")
generator_url = f"https://github.com/fish-shell/fish-shell/blob/{base_ref_oid}/.github/workflows/update-event-feeds.yml"
fg.link(href=generator_url, rel="alternate")
entries = {}
if feed_path.exists():
tree = ET.parse(feed_path)
root = tree.getroot()
xmlns = "{http://www.w3.org/2005/Atom}"
for child in root.findall(f"{xmlns}entry"):
pr_url = child.find(f"{xmlns}id").text
pr_title = child.find(f"{xmlns}title").text
updated = child.find(f"{xmlns}updated").text
updated = datetime.fromisoformat(updated)
entries[pr_url] = (pr_title, updated)
if this_pr is not None:
(this_pr_title, this_pr_url) = this_pr
if this_pr_url not in entries:
entries[this_pr_url] = (this_pr_title, datetime.now(dateutil.tz.tzutc()))
for pr_url, (pr_title, updated) in entries.items():
fe = fg.add_entry()
fe.id(pr_url)
fe.title(pr_title)
fe.updated(updated)
fe.link(href=pr_url)
atomfeed = fg.atom_str(pretty=True).decode()
feed_path.write_text(atomfeed)
def pull_request_data(pr_number: str):
data = json.loads(
subprocess.check_output(
(
"gh",
"pr",
"view",
"--json",
"baseRefOid,headRefOid,title,url",
str(pr_number),
),
text=True,
)
)
base_ref_oid = data["baseRefOid"]
head_ref_oid = data["headRefOid"]
this_pr_title = data["title"]
this_pr_url = data["url"]
return (base_ref_oid, head_ref_oid, this_pr_title, this_pr_url)
def main():
pr_number = sys.argv[1]
(base_ref_oid, head_ref_oid, this_pr_title, this_pr_url) = pull_request_data(
pr_number
)
repo = git.Repo()
repo.git().execute(("git", "fetch", "origin", base_ref_oid, head_ref_oid))
po_files = (
Path(file)
for file in repo.git()
.execute(("git", "ls-tree", "--name-only", base_ref_oid, ":/po/"))
.splitlines()
)
changed_files = {
Path(file)
for file in subprocess.check_output(
("git", "diff", "--name-only", base_ref_oid, head_ref_oid), text=True
).splitlines()
if re.match(r"^po/\w+\.po$", file)
}
Path("po").mkdir(exist_ok=True)
for po_file in po_files:
file_without_extension = str(po_file).removesuffix(".po")
ll_CC = os.path.basename(file_without_extension)
feed_additions_path = Path(f"{file_without_extension}-additions.atom")
feed_removals_path = Path(f"{file_without_extension}-removals.atom")
def title(verb: str):
return f"fish-shell PRs {verb} {ll_CC} translations"
def write_additions(this_pr):
subtitle = "PRs that add new translations"
write_feed(
feed_additions_path, base_ref_oid, title("adding"), subtitle, this_pr
)
def write_removals(this_pr):
subtitle = "PRs that remove translations, often due to a change in the messages source"
write_feed(
feed_removals_path, base_ref_oid, title("removing"), subtitle, this_pr
)
if not feed_additions_path.exists():
write_additions(None)
if not feed_removals_path.exists():
write_removals(None)
if po_file not in changed_files:
continue
old = message_dictionary(repo, base_ref_oid, po_file)
new = message_dictionary(repo, head_ref_oid, po_file)
if any(
new_msgstr != old.get(new_msgid) and new_msgstr != ""
for (new_msgid, new_msgstr) in new.items()
):
write_additions((this_pr_title, this_pr_url))
elif any(
old_msgstr != "" and new.get(old_msgid, "") == ""
for (old_msgid, old_msgstr) in old.items()
):
write_removals((this_pr_title, this_pr_url))
if __name__ == "__main__":
main()

View File

@@ -41,7 +41,7 @@ begin
mark_section tier1-from-rust
# Get rid of duplicates and sort.
msguniq --no-wrap --strict --sort-output $rust_extraction_file
msguniq --no-wrap --sort-output $rust_extraction_file
or exit 1
if not set -l --query _flag_use_existing_template

View File

@@ -147,15 +147,21 @@ rm -rf "$tmpdir"
" | sed 's,^\s*| \?,,')"
)
# Approve macos-codesign
# TODO what if current user can't approve?
gh_pending_deployments() {
gh_api_repo() {
path=$1
shift
command gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$repository_owner/fish-shell/actions/runs/$run_id/pending_deployments" \
"/repos/$repository_owner/fish-shell/$1" \
"$@"
}
# Approve macos-codesign
# TODO what if current user can't approve?
gh_pending_deployments() {
gh_api_repo "actions/runs/$run_id/pending_deployments" "$@"
}
while {
environment_id=$(gh_pending_deployments | jq .[].environment.id)
[ -z "$environment_id" ]
@@ -170,7 +176,7 @@ echo '
"comment": "Approved via ./build_tools/release.sh"
}
' |
gh_pending_deployments -XPOST --input=-
gh_pending_deployments --method POST --input=-
# Await completion.
gh run watch "$run_id"
@@ -203,7 +209,7 @@ done
if [ -n "$integration_branch" ]; then {
git push $remote "$version^{commit}":refs/heads/$integration_branch
else
} else {
changelog=$(cat - CHANGELOG.rst <<EOF
fish ?.?.? (released ???)
=========================
@@ -215,6 +221,25 @@ EOF
git push $remote HEAD:master
} fi
milestone_number=$(
gh_api_repo milestones?state=open |
jq '.[] | select(.title == "fish '"$version"'") | .number'
)
gh_api_repo --method PATCH milestones/$milestone_number \
--raw-field state=closed
next_patch_version=$(
echo "$version" | awk -F. '
NF == 3 && $3 ~ /[0-9]+/ {
printf "%s.%s.%s", $1, $2, $3+1
}
'
)
if [ -n "$next_patch_version" ]; then
gh_api_repo --method POST milestones \
--raw-field title="fish $next_patch_version"
fi
exit
}

View File

@@ -101,14 +101,10 @@ end
# This is used to identify lines which should be set here via $header_lines.
# Make sure that this prefix does not appear elsewhere in the file and only contains characters
# without special meaning in a sed pattern.
set -g header_prefix "# fish-note: "
set -g header_prefix "# fish-note-sections: "
function print_header -a po_file
set -l ll_CC (basename $po_file .po)
function print_header
set -l header_lines \
"To subscribe to proposed updates, point your feed reader at:" \
https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/$ll_CC-additions.atom \
https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/$ll_CC-removals.atom \
"Translations are divided into sections, each starting with a fish-section-* pseudo-message." \
"The first few sections are more important." \
"Ignore the tier3 sections unless you have a lot of time."
@@ -127,7 +123,7 @@ function merge_po_files --argument-names template_file po_file
or cleanup_exit
begin
print_header $po_file
print_header
# Paste PO file without old header lines.
sed '/^'$header_prefix'/d' $new_po_file
end >$po_file
@@ -143,7 +139,7 @@ for po_file in $po_files
merge_po_files $template_file $po_file
else
begin
print_header $po_file
print_header
cat $template_file
end >$po_file
end

View File

@@ -1,9 +1,6 @@
# fish-note: To subscribe to proposed updates, point your feed reader at:
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/de-additions.atom
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/de-removals.atom
# fish-note: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note: The first few sections are more important.
# fish-note: Ignore the tier3 sections unless you have a lot of time.
# fish-note-sections: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note-sections: The first few sections are more important.
# fish-note-sections: Ignore the tier3 sections unless you have a lot of time.
# translation of de.po to deutsch
# This file is distributed under the same license as the PACKAGE package.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER.

View File

@@ -1,9 +1,6 @@
# fish-note: To subscribe to proposed updates, point your feed reader at:
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/en-additions.atom
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/en-removals.atom
# fish-note: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note: The first few sections are more important.
# fish-note: Ignore the tier3 sections unless you have a lot of time.
# fish-note-sections: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note-sections: The first few sections are more important.
# fish-note-sections: Ignore the tier3 sections unless you have a lot of time.
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.

View File

@@ -1,9 +1,6 @@
# fish-note: To subscribe to proposed updates, point your feed reader at:
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/fr-additions.atom
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/fr-removals.atom
# fish-note: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note: The first few sections are more important.
# fish-note: Ignore the tier3 sections unless you have a lot of time.
# fish-note-sections: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note-sections: The first few sections are more important.
# fish-note-sections: Ignore the tier3 sections unless you have a lot of time.
# translation of fr.po to Français
# FRENCH TRANSLATION FOR FISH
# Copyright (C) 2006 Xavier Douville

View File

@@ -1,9 +1,6 @@
# fish-note: To subscribe to proposed updates, point your feed reader at:
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/pl-additions.atom
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/pl-removals.atom
# fish-note: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note: The first few sections are more important.
# fish-note: Ignore the tier3 sections unless you have a lot of time.
# fish-note-sections: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note-sections: The first few sections are more important.
# fish-note-sections: Ignore the tier3 sections unless you have a lot of time.
# Translation of fish in Polish
# This file is distributed under the same license as the fish package.
# Author: m4sk1n <m4sk1n@vivaldi.net>

View File

@@ -1,9 +1,6 @@
# fish-note: To subscribe to proposed updates, point your feed reader at:
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/pt_BR-additions.atom
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/pt_BR-removals.atom
# fish-note: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note: The first few sections are more important.
# fish-note: Ignore the tier3 sections unless you have a lot of time.
# fish-note-sections: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note-sections: The first few sections are more important.
# fish-note-sections: Ignore the tier3 sections unless you have a lot of time.
# Portuguese translations for fish package.
# Copyright (C) 2014 THE fish'S COPYRIGHT HOLDER
# This file is distributed under the same license as the fish package.

View File

@@ -1,9 +1,6 @@
# fish-note: To subscribe to proposed updates, point your feed reader at:
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/sv-additions.atom
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/sv-removals.atom
# fish-note: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note: The first few sections are more important.
# fish-note: Ignore the tier3 sections unless you have a lot of time.
# fish-note-sections: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note-sections: The first few sections are more important.
# fish-note-sections: Ignore the tier3 sections unless you have a lot of time.
# Copyright © 2006
# This file is distributed under the same license as the fish package.
# Axel Liljencrantz <liljencrantz@gmail.com>, 2006

View File

@@ -1,9 +1,6 @@
# fish-note: To subscribe to proposed updates, point your feed reader at:
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/zh_CN-additions.atom
# fish-note: https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/zh_CN-removals.atom
# fish-note: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note: The first few sections are more important.
# fish-note: Ignore the tier3 sections unless you have a lot of time.
# fish-note-sections: Translations are divided into sections, each starting with a fish-section-* pseudo-message.
# fish-note-sections: The first few sections are more important.
# fish-note-sections: Ignore the tier3 sections unless you have a lot of time.
#
# General Notices 凡例
# abort: 中止
@@ -3661,7 +3658,7 @@ msgid "Vi mode commands"
msgstr "Vi 模式命令"
msgid "Vi-style bindings that inherit emacs-style bindings in all modes"
msgstr "所有模式中的继承 emacs 样式绑定的 Vi 样式绑定"
msgstr "所有模式中的继承 emacs 样式绑定的 vi 样式绑定"
msgid "View and pick from the sample prompts"
msgstr "查看并从示例提示中选择"

View File

@@ -47,7 +47,7 @@ complete -c scp -d "Local Path" -n "not string match @ -- (commandline -ct)"
complete -c scp -d "Remote Path" -f -n "commandline -ct | string match -e ':'" -a '
(__scp_remote_target):(
if not set -q __fish_scp_sftp
set -l tmp (__fish_mktemp fish-scp)
set -l tmp (__fish_mktemp_relative fish-scp)
if scp -P(__scp2ssh_port_number) -o "BatchMode yes" -q -O $tmp (__scp_remote_target):/dev/null
set -g __fish_scp_sftp true
else

View File

@@ -229,7 +229,7 @@ def parse_color(color_str):
background_color = ""
underline_color = ""
bold = False
underline = False
underline = None
italics = False
dim = False
reverse = False
@@ -263,6 +263,10 @@ def parse_color(color_str):
) -> str:
if comp.startswith(long_opt):
c = comp[len(long_opt) :]
if c[0] == "=":
# There was a = between the long option and the value.
# i.e. support also --background=red, not just --background red
c = c[1:]
parsed_c = parse_one_color(c)
# We prefer the unparsed version - if it says "brgreen", we use brgreen,
# instead of 00ff00

View File

@@ -24,6 +24,7 @@
TOK_ACCEPT_UNFINISHED, TOK_ARGUMENT_LIST, TOK_CONTINUE_AFTER_ERROR, TOK_SHOW_COMMENTS,
};
use crate::wchar::prelude::*;
use macro_rules_attribute::derive;
use std::borrow::Cow;
use std::convert::AsMut;
use std::ops::{ControlFlow, Deref};
@@ -404,8 +405,8 @@ trait CheckParse: Default {
}
/// Implement the node trait.
macro_rules! implement_node {
( $name:ident ) => {
macro_rules! Node {
($name:ident) => {
impl Node for $name {
fn kind(&self) -> Kind<'_> {
Kind::$name(self)
@@ -425,11 +426,19 @@ fn cast(node: &dyn Node) -> Option<&Self> {
}
}
};
( $(#[$_m:meta])* $_v:vis struct $name:ident $_:tt $(;)? ) => {
Node!($name);
};
( $(#[$_m:meta])* $_v:vis enum $name:ident $_:tt ) => {
Node!($name);
};
}
/// Implement the leaf trait.
macro_rules! implement_leaf {
( $name:ident ) => {
macro_rules! Leaf {
($name:ident) => {
impl Leaf for $name {
fn range(&self) -> Option<SourceRange> {
self.range
@@ -450,17 +459,20 @@ fn accept_mut<V: NodeVisitorMut>(&mut self, visitor: &mut V) {
}
}
};
( $(#[$_m:meta])* $_v:vis struct $name:ident $_:tt $(;)? ) => {
Leaf!($name);
};
}
/// Define a node that implements the keyword trait.
macro_rules! define_keyword_node {
( $name:ident, $($allowed:ident),* $(,)? ) => {
#[derive(Default, Debug)]
#[derive(Default, Debug, Leaf!)]
pub struct $name {
range: Option<SourceRange>,
keyword: ParseKeyword,
}
implement_leaf!($name);
impl Node for $name {
fn kind(&self) -> Kind<'_> {
Kind::Keyword(self)
@@ -489,7 +501,7 @@ fn as_leaf(&self) -> &dyn Leaf {
/// Define a node that implements the token trait.
macro_rules! define_token_node {
( $name:ident, $($allowed:ident),* $(,)? ) => {
#[derive(Default, Debug)]
#[derive(Default, Debug, Leaf!)]
pub struct $name {
range: Option<SourceRange>,
parse_token_type: ParseTokenType,
@@ -502,7 +514,6 @@ fn kind_mut(&mut self) -> KindMut<'_> {
KindMut::Token(self)
}
}
implement_leaf!($name);
impl Token for $name {
fn token_type(&self) -> ParseTokenType {
self.parse_token_type
@@ -535,11 +546,9 @@ macro_rules! define_list_node {
$name:ident,
$contents:ident
) => {
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!)]
pub struct $name(Box<[$contents]>);
implement_node!($name);
impl Deref for $name {
type Target = Box<[$contents]>;
fn deref(&self) -> &Self::Target {
@@ -582,11 +591,15 @@ fn accept_mut<V: NodeVisitorMut>(&mut self, visitor: &mut V) {
}
/// Implement the acceptor trait for the given branch node.
macro_rules! implement_acceptor_for_branch {
macro_rules! Acceptor {
(
$name:ident
$(, $field_name:ident )*
$(,)?
$(#[$_m:meta])*
$_v:vis struct $name:ident {
$(
$(#[$_fm:meta])*
$_fv:vis $field_name:ident : $_ft:ty
),* $(,)?
}
) => {
impl Acceptor for $name {
#[allow(unused_variables)]
@@ -612,18 +625,16 @@ fn accept_mut<V: NodeVisitorMut>(&mut self, visitor: &mut V) {
visitor.did_visit_fields_of(self, flow);
}
}
}
};
}
/// A redirection has an operator like > or 2>, and a target like /dev/null or &1.
/// Note that pipes are not redirections.
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct Redirection {
pub oper: TokenRedirection,
pub target: String_,
}
implement_node!(Redirection);
implement_acceptor_for_branch!(Redirection, oper, target);
impl CheckParse for Redirection {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
@@ -633,7 +644,7 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
define_list_node!(VariableAssignmentList, VariableAssignment);
#[derive(Debug)]
#[derive(Debug, Node!)]
pub enum ArgumentOrRedirection {
Argument(Argument),
Redirection(Box<Redirection>), // Boxed because it's bigger
@@ -689,8 +700,6 @@ pub fn redirection(&self) -> &Redirection {
}
}
implement_node!(ArgumentOrRedirection);
impl CheckParse for ArgumentOrRedirection {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
let typ = pop.peek_type(0);
@@ -701,7 +710,7 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
define_list_node!(ArgumentOrRedirectionList, ArgumentOrRedirection);
/// A statement is a normal command, or an if / while / etc
#[derive(Debug)]
#[derive(Debug, Node!)]
pub enum Statement {
Decorated(DecoratedStatement),
Not(Box<NotStatement>),
@@ -710,7 +719,6 @@ pub enum Statement {
If(Box<IfStatement>),
Switch(Box<SwitchStatement>),
}
implement_node!(Statement);
impl Default for Statement {
fn default() -> Self {
@@ -756,7 +764,7 @@ fn accept_mut<V: NodeVisitorMut>(&mut self, visitor: &mut V) {
/// A job is a non-empty list of statements, separated by pipes. (Non-empty is useful for cases
/// like if statements, where we require a command).
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct JobPipeline {
/// Maybe the time keyword.
pub time: Option<KeywordTime>,
@@ -769,11 +777,9 @@ pub struct JobPipeline {
/// Maybe backgrounded.
pub bg: Option<TokenBackground>,
}
implement_node!(JobPipeline);
implement_acceptor_for_branch!(JobPipeline, time, variables, statement, continuation, bg);
/// A job_conjunction is a job followed by a && or || continuations.
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct JobConjunction {
/// The job conjunction decorator.
pub decorator: Option<JobConjunctionDecorator>,
@@ -786,8 +792,6 @@ pub struct JobConjunction {
/// only fail to be present if we ran out of tokens.
pub semi_nl: Option<SemiNl>,
}
implement_node!(JobConjunction);
implement_acceptor_for_branch!(JobConjunction, decorator, job, continuations, semi_nl);
impl CheckParse for JobConjunction {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
@@ -802,7 +806,7 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
}
}
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct ForHeader {
/// 'for'
pub kw_for: KeywordFor,
@@ -815,20 +819,16 @@ pub struct ForHeader {
/// newline or semicolon
pub semi_nl: SemiNl,
}
implement_node!(ForHeader);
implement_acceptor_for_branch!(ForHeader, kw_for, var_name, kw_in, args, semi_nl);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct WhileHeader {
/// 'while'
pub kw_while: KeywordWhile,
pub condition: JobConjunction,
pub andor_tail: AndorJobList,
}
implement_node!(WhileHeader);
implement_acceptor_for_branch!(WhileHeader, kw_while, condition, andor_tail);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct FunctionHeader {
pub kw_function: KeywordFunction,
/// functions require at least one argument.
@@ -836,20 +836,16 @@ pub struct FunctionHeader {
pub args: ArgumentList,
pub semi_nl: SemiNl,
}
implement_node!(FunctionHeader);
implement_acceptor_for_branch!(FunctionHeader, kw_function, first_arg, args, semi_nl);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct BeginHeader {
pub kw_begin: KeywordBegin,
/// Note that 'begin' does NOT require a semi or nl afterwards.
/// This is valid: begin echo hi; end
pub semi_nl: Option<SemiNl>,
}
implement_node!(BeginHeader);
implement_acceptor_for_branch!(BeginHeader, kw_begin, semi_nl);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct BlockStatement {
/// A header like for, while, etc.
pub header: BlockStatementHeader,
@@ -860,10 +856,8 @@ pub struct BlockStatement {
/// Arguments and redirections associated with the block.
pub args_or_redirs: ArgumentOrRedirectionList,
}
implement_node!(BlockStatement);
implement_acceptor_for_branch!(BlockStatement, header, jobs, end, args_or_redirs);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct BraceStatement {
/// The opening brace, in command position.
pub left_brace: TokenLeftBrace,
@@ -874,16 +868,8 @@ pub struct BraceStatement {
/// Arguments and redirections associated with the block.
pub args_or_redirs: ArgumentOrRedirectionList,
}
implement_node!(BraceStatement);
implement_acceptor_for_branch!(
BraceStatement,
left_brace,
jobs,
right_brace,
args_or_redirs
);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct IfClause {
/// The 'if' keyword.
pub kw_if: KeywordIf,
@@ -894,18 +880,14 @@ pub struct IfClause {
/// The body to execute if the condition is true.
pub body: JobList,
}
implement_node!(IfClause);
implement_acceptor_for_branch!(IfClause, kw_if, condition, andor_tail, body);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct ElseifClause {
/// The 'else' keyword.
pub kw_else: KeywordElse,
/// The 'if' clause following it.
pub if_clause: IfClause,
}
implement_node!(ElseifClause);
implement_acceptor_for_branch!(ElseifClause, kw_else, if_clause);
impl CheckParse for ElseifClause {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
pop.peek_token(0).keyword == ParseKeyword::Else
@@ -915,22 +897,20 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
define_list_node!(ElseifClauseList, ElseifClause);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct ElseClause {
/// else ; body
pub kw_else: KeywordElse,
pub semi_nl: Option<SemiNl>,
pub body: JobList,
}
implement_node!(ElseClause);
implement_acceptor_for_branch!(ElseClause, kw_else, semi_nl, body);
impl CheckParse for ElseClause {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
pop.peek_token(0).keyword == ParseKeyword::Else
}
}
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct IfStatement {
/// if part
pub if_clause: IfClause,
@@ -943,17 +923,8 @@ pub struct IfStatement {
/// block args / redirs
pub args_or_redirs: ArgumentOrRedirectionList,
}
implement_node!(IfStatement);
implement_acceptor_for_branch!(
IfStatement,
if_clause,
elseif_clauses,
else_clause,
end,
args_or_redirs
);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct CaseItem {
/// case \<arguments\> ; body
pub kw_case: KeywordCase,
@@ -961,15 +932,13 @@ pub struct CaseItem {
pub semi_nl: SemiNl,
pub body: JobList,
}
implement_node!(CaseItem);
implement_acceptor_for_branch!(CaseItem, kw_case, arguments, semi_nl, body);
impl CheckParse for CaseItem {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
pop.peek_token(0).keyword == ParseKeyword::Case
}
}
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct SwitchStatement {
/// switch \<argument\> ; body ; end args_redirs
pub kw_switch: KeywordSwitch,
@@ -979,20 +948,10 @@ pub struct SwitchStatement {
pub end: KeywordEnd,
pub args_or_redirs: ArgumentOrRedirectionList,
}
implement_node!(SwitchStatement);
implement_acceptor_for_branch!(
SwitchStatement,
kw_switch,
argument,
semi_nl,
cases,
end,
args_or_redirs
);
/// A decorated_statement is a command with a list of arguments_or_redirections, possibly with
/// "builtin" or "command" or "exec"
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct DecoratedStatement {
/// An optional decoration (command, builtin, exec, etc).
pub opt_decoration: Option<DecoratedStatementDecorator>,
@@ -1001,11 +960,9 @@ pub struct DecoratedStatement {
/// Args and redirs
pub args_or_redirs: ArgumentOrRedirectionList,
}
implement_node!(DecoratedStatement);
implement_acceptor_for_branch!(DecoratedStatement, opt_decoration, command, args_or_redirs);
/// A not statement like `not true` or `! true`
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct NotStatement {
/// Keyword, either not or exclam.
pub kw: KeywordNot,
@@ -1013,18 +970,14 @@ pub struct NotStatement {
pub variables: VariableAssignmentList,
pub contents: Statement,
}
implement_node!(NotStatement);
implement_acceptor_for_branch!(NotStatement, kw, time, variables, contents);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct JobContinuation {
pub pipe: TokenPipe,
pub newlines: MaybeNewlines,
pub variables: VariableAssignmentList,
pub statement: Statement,
}
implement_node!(JobContinuation);
implement_acceptor_for_branch!(JobContinuation, pipe, newlines, variables, statement);
impl CheckParse for JobContinuation {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
pop.peek_type(0) == ParseTokenType::pipe
@@ -1033,7 +986,7 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
define_list_node!(JobContinuationList, JobContinuation);
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct JobConjunctionContinuation {
/// The && or || token.
pub conjunction: TokenConjunction,
@@ -1041,8 +994,6 @@ pub struct JobConjunctionContinuation {
/// The job itself.
pub job: JobPipeline,
}
implement_node!(JobConjunctionContinuation);
implement_acceptor_for_branch!(JobConjunctionContinuation, conjunction, newlines, job);
impl CheckParse for JobConjunctionContinuation {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
let typ = pop.peek_type(0);
@@ -1053,12 +1004,10 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
/// An andor_job just wraps a job, but requires that the job have an 'and' or 'or' job_decorator.
/// Note this is only used for andor_job_list; jobs that are not part of an andor_job_list are not
/// instances of this.
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct AndorJob {
pub job: JobConjunction,
}
implement_node!(AndorJob);
implement_acceptor_for_branch!(AndorJob, job);
impl CheckParse for AndorJob {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
let keyword = pop.peek_token(0).keyword;
@@ -1080,12 +1029,10 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
/// A freestanding_argument_list is equivalent to a normal argument list, except it may contain
/// TOK_END (newlines, and even semicolons, for historical reasons).
/// In practice the tok_ends are ignored by fish code so we do not bother to store them.
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Acceptor!)]
pub struct FreestandingArgumentList {
pub arguments: ArgumentList,
}
implement_node!(FreestandingArgumentList);
implement_acceptor_for_branch!(FreestandingArgumentList, arguments);
define_list_node!(JobConjunctionContinuationList, JobConjunctionContinuation);
@@ -1097,12 +1044,10 @@ pub struct FreestandingArgumentList {
define_list_node!(CaseItemList, CaseItem);
/// A variable_assignment contains a source range like FOO=bar.
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Leaf!)]
pub struct VariableAssignment {
range: Option<SourceRange>,
}
implement_node!(VariableAssignment);
implement_leaf!(VariableAssignment);
impl CheckParse for VariableAssignment {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
// Do we have a variable assignment at all?
@@ -1124,21 +1069,17 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
}
/// Zero or more newlines.
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Leaf!)]
pub struct MaybeNewlines {
range: Option<SourceRange>,
}
implement_node!(MaybeNewlines);
implement_leaf!(MaybeNewlines);
/// An argument is just a node whose source range determines its contents.
/// This is a separate type because it is sometimes useful to find all arguments.
#[derive(Default, Debug)]
#[derive(Default, Debug, Node!, Leaf!)]
pub struct Argument {
range: Option<SourceRange>,
}
implement_node!(Argument);
implement_leaf!(Argument);
impl CheckParse for Argument {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
pop.peek_type(0) == ParseTokenType::string
@@ -1226,14 +1167,13 @@ pub fn decoration(&self) -> StatementDecoration {
}
}
#[derive(Debug)]
#[derive(Debug, Node!)]
pub enum BlockStatementHeader {
Begin(BeginHeader),
For(ForHeader),
While(WhileHeader),
Function(FunctionHeader),
}
implement_node!(BlockStatementHeader);
impl Default for BlockStatementHeader {
fn default() -> Self {

View File

@@ -246,20 +246,18 @@ fn handle_sprintf_error(&mut self, err: fish_printf::Error) {
}
}
/// Evaluate a printf conversion specification. SPEC is the start of the directive, and CONVERSION
/// specifies the type of conversion. SPEC does not include any length modifier or the
/// conversion specifier itself. FIELD_WIDTH and PRECISION are the field width and
/// precision for '*' values, if HAVE_FIELD_WIDTH and HAVE_PRECISION are true, respectively.
/// ARGUMENT is the argument to be formatted.
/// Evaluate a printf conversion specification.
/// `spec` is the start of the directive, and `conversion` specifies the type of conversion.
/// `spec` does not include any length modifier or the conversion specifier itself.
/// `field_width` and `precision` are the field width and precision for '*' values, if any.
/// `argument` is the argument to be formatted.
#[allow(clippy::collapsible_else_if, clippy::too_many_arguments)]
fn print_direc(
fn print_directive(
&mut self,
spec: &wstr,
conversion: char,
have_field_width: bool,
field_width: i32,
have_precision: bool,
precision: i32,
field_width: Option<i64>,
precision: Option<i64>,
argument: &wstr,
) {
/// Printf macro helper which provides our locale.
@@ -283,21 +281,6 @@ macro_rules! append_output_fmt {
// Start with everything except the conversion specifier.
let mut fmt = spec.to_owned();
// Create a copy of the % directive, with a width modifier substituted for any
// existing integer length modifier.
match conversion {
'x' | 'X' | 'd' | 'i' | 'o' | 'u' => {
fmt.push_str("ll");
}
'a' | 'e' | 'f' | 'g' | 'A' | 'E' | 'F' | 'G' => {
fmt.push_str("L");
}
's' | 'c' => {
fmt.push_str("l");
}
_ => {}
}
// Append the conversion itself.
fmt.push(conversion);
@@ -306,77 +289,96 @@ macro_rules! append_output_fmt {
match conversion {
'd' | 'i' => {
let arg: i64 = string_to_scalar_type(argument, self);
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, arg);
} else {
append_output_fmt!(fmt, precision, arg);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, arg);
} else {
append_output_fmt!(fmt, field_width, precision, arg);
}
match field_width {
Some(field_width) => match precision {
Some(precision) => {
append_output_fmt!(fmt, field_width, precision, arg);
}
None => {
append_output_fmt!(fmt, field_width, arg);
}
},
None => match precision {
Some(precision) => {
append_output_fmt!(fmt, precision, arg);
}
None => {
append_output_fmt!(fmt, arg);
}
},
}
}
'o' | 'u' | 'x' | 'X' => {
let arg: u64 = string_to_scalar_type(argument, self);
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, arg);
} else {
append_output_fmt!(fmt, precision, arg);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, arg);
} else {
append_output_fmt!(fmt, field_width, precision, arg);
}
match field_width {
Some(field_width) => match precision {
Some(precision) => {
append_output_fmt!(fmt, field_width, precision, arg);
}
None => {
append_output_fmt!(fmt, field_width, arg);
}
},
None => match precision {
Some(precision) => {
append_output_fmt!(fmt, precision, arg);
}
None => {
append_output_fmt!(fmt, arg);
}
},
}
}
'a' | 'A' | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' => {
let arg: f64 = string_to_scalar_type(argument, self);
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, arg);
} else {
append_output_fmt!(fmt, precision, arg);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, arg);
} else {
append_output_fmt!(fmt, field_width, precision, arg);
}
match field_width {
Some(field_width) => match precision {
Some(precision) => {
append_output_fmt!(fmt, field_width, precision, arg);
}
None => {
append_output_fmt!(fmt, field_width, arg);
}
},
None => match precision {
Some(precision) => {
append_output_fmt!(fmt, precision, arg);
}
None => {
append_output_fmt!(fmt, arg);
}
},
}
}
'c' => {
if !have_field_width {
append_output_fmt!(fmt, argument.char_at(0));
} else {
'c' => match field_width {
Some(field_width) => {
append_output_fmt!(fmt, field_width, argument.char_at(0));
}
}
None => {
append_output_fmt!(fmt, argument.char_at(0));
}
},
's' => {
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, argument);
} else {
append_output_fmt!(fmt, precision, argument);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, argument);
} else {
's' => match field_width {
Some(field_width) => match precision {
Some(precision) => {
append_output_fmt!(fmt, field_width, precision, argument);
}
}
}
None => {
append_output_fmt!(fmt, field_width, argument);
}
},
None => match precision {
Some(precision) => {
append_output_fmt!(fmt, precision, argument);
}
None => {
append_output_fmt!(fmt, argument);
}
},
},
_ => {
panic!("unexpected opt: {}", conversion);
@@ -384,18 +386,16 @@ macro_rules! append_output_fmt {
}
}
/// Print the text in FORMAT, using ARGV for arguments to any `%' directives.
/// Return the number of elements of ARGV used.
/// Print the text in `format`, using `argv` for arguments to any `%' directives.
/// Return the number of elements of `argv` used.
fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
let mut argc = argv.len();
let save_argc = argc; /* Preserve original value. */
let mut f: &wstr; /* Pointer into `format'. */
let mut direc_start: &wstr; /* Start of % directive. */
let mut direc_length: usize; /* Length of % directive. */
let mut have_field_width: bool; /* True if FIELD_WIDTH is valid. */
let mut field_width: c_int = 0; /* Arg to first '*'. */
let mut have_precision: bool; /* True if PRECISION is valid. */
let mut precision = 0; /* Arg to second '*'. */
let mut directive_start: &wstr; /* Start of % directive. */
let mut directive_length: usize; /* Length of % directive. */
let mut field_width: Option<i64>; /* Arg to first '*'. */
let mut precision: Option<i64>; /* Arg to second '*'. */
let mut ok = [false; 256]; /* ok['x'] is true if %x is allowed. */
// N.B. this was originally written as a loop like so:
@@ -414,11 +414,11 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
match f.char_at(0) {
'%' => {
direc_start = f;
directive_start = f;
f = &f[1..];
direc_length = 1;
have_field_width = false;
have_precision = false;
directive_length = 1;
field_width = None;
precision = None;
if f.char_at(0) == '%' {
self.append_output('%');
continue;
@@ -460,17 +460,17 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
}
if continue_looking_for_flags {
f = &f[1..];
direc_length += 1;
directive_length += 1;
}
}
if f.char_at(0) == '*' {
f = &f[1..];
direc_length += 1;
directive_length += 1;
if argc > 0 {
let width: i64 = string_to_scalar_type(argv[0], self);
if (c_int::MIN as i64) <= width && width <= (c_int::MAX as i64) {
field_width = width as c_int;
field_width = Some(width);
} else {
self.fatal_error(wgettext_fmt!(
"invalid field width: %ls",
@@ -480,47 +480,45 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
argv = &argv[1..];
argc -= 1;
} else {
field_width = 0;
field_width = Some(0);
}
have_field_width = true;
} else {
while iswdigit(f.char_at(0)) {
f = &f[1..];
direc_length += 1;
directive_length += 1;
}
}
if f.char_at(0) == '.' {
f = &f[1..];
direc_length += 1;
directive_length += 1;
modify_allowed_format_specifiers(&mut ok, "c", false);
if f.char_at(0) == '*' {
f = &f[1..];
direc_length += 1;
directive_length += 1;
if argc > 0 {
let prec: i64 = string_to_scalar_type(argv[0], self);
if prec < 0 {
// A negative precision is taken as if the precision were omitted,
// so -1 is safe here even if prec < INT_MIN.
precision = -1;
precision = Some(-1);
} else if (c_int::MAX as i64) < prec {
self.fatal_error(wgettext_fmt!(
"invalid precision: %ls",
argv[0]
));
} else {
precision = prec as c_int;
precision = Some(prec);
}
argv = &argv[1..];
argc -= 1;
} else {
precision = 0;
precision = Some(0);
}
have_precision = true;
} else {
while iswdigit(f.char_at(0)) {
f = &f[1..];
direc_length += 1;
directive_length += 1;
}
}
}
@@ -533,8 +531,8 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
if (conversion as usize) > 0xFF || !ok[conversion as usize] {
self.fatal_error(wgettext_fmt!(
"%.*ls: invalid conversion specification",
wstr_offset_in(f, direc_start) + 1,
direc_start
wstr_offset_in(f, directive_start) + 1,
directive_start
));
return 0;
}
@@ -545,12 +543,10 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
argv = &argv[1..];
argc -= 1;
}
self.print_direc(
&direc_start[..direc_length],
self.print_directive(
&directive_start[..directive_length],
f.char_at(0),
have_field_width,
field_width,
have_precision,
precision,
argument,
);

View File

@@ -5,11 +5,88 @@
use once_cell::sync::Lazy;
use crate::fallback::{fish_wcswidth, wcscasecmp};
use crate::libc::*;
use crate::wutil::perror;
use super::prelude::*;
pub mod limits {
/// Constants that exist everywhere (Linux, macOS, BSD).
/// Note these are uints on Linux but ints everywhere else - we use -1 as a sentinel
/// so cast to int.
pub mod common {
use libc;
pub const CORE: libc::c_int = libc::RLIMIT_CORE as _;
pub const DATA: libc::c_int = libc::RLIMIT_DATA as _;
pub const FSIZE: libc::c_int = libc::RLIMIT_FSIZE as _;
pub const MEMLOCK: libc::c_int = libc::RLIMIT_MEMLOCK as _;
pub const NOFILE: libc::c_int = libc::RLIMIT_NOFILE as _;
pub const STACK: libc::c_int = libc::RLIMIT_STACK as _;
pub const CPU: libc::c_int = libc::RLIMIT_CPU as _;
pub const NPROC: libc::c_int = libc::RLIMIT_NPROC as _;
pub const AS: libc::c_int = libc::RLIMIT_AS as _;
}
pub use self::common::*;
#[cfg(target_os = "linux")]
pub mod linux {
use libc;
pub const SIGPENDING: libc::c_int = libc::RLIMIT_SIGPENDING as _;
pub const MSGQUEUE: libc::c_int = libc::RLIMIT_MSGQUEUE as _;
pub const RTPRIO: libc::c_int = libc::RLIMIT_RTPRIO as _;
pub const RTTIME: libc::c_int = libc::RLIMIT_RTTIME as _;
pub const RSS: libc::c_int = libc::RLIMIT_RSS as _;
pub const SBSIZE: libc::c_int = -1;
pub const NICE: libc::c_int = -1;
pub const SWAP: libc::c_int = -1;
pub const KQUEUES: libc::c_int = -1;
pub const NPTS: libc::c_int = -1;
pub const NTHR: libc::c_int = -1;
}
#[cfg(target_os = "linux")]
pub use self::linux::*;
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub mod macos {
use libc;
pub const SIGPENDING: libc::c_int = -1;
pub const MSGQUEUE: libc::c_int = -1;
pub const RTPRIO: libc::c_int = -1;
pub const RTTIME: libc::c_int = -1;
pub const SBSIZE: libc::c_int = -1;
pub const NICE: libc::c_int = -1;
pub const RSS: libc::c_int = -1;
pub const SWAP: libc::c_int = -1;
pub const KQUEUES: libc::c_int = -1;
pub const NPTS: libc::c_int = -1;
pub const NTHR: libc::c_int = -1;
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub use self::macos::*;
#[cfg(bsd)]
pub mod bsd {
use libc;
pub const SBSIZE: libc::c_int = libc::RLIMIT_SBSIZE;
pub const NICE: libc::c_int = libc::RLIMIT_NICE;
pub const RSS: libc::c_int = libc::RLIMIT_RSS;
pub const NTHR: libc::c_int = libc::RLIMIT_NTHR;
pub const SWAP: libc::c_int = libc::RLIMIT_SWAP;
pub const KQUEUES: libc::c_int = libc::RLIMIT_KQUEUES;
pub const NPTS: libc::c_int = libc::RLIMIT_NPTS;
pub const SIGPENDING: libc::c_int = -1;
pub const MSGQUEUE: libc::c_int = -1;
pub const RTPRIO: libc::c_int = -1;
pub const RTTIME: libc::c_int = -1;
}
#[cfg(bsd)]
pub use self::bsd::*;
}
/// Calls getrlimit.
fn getrlimit(resource: c_uint) -> Option<(rlim_t, rlim_t)> {
let resource: i32 = resource.try_into().unwrap();
@@ -57,7 +134,7 @@ fn print_all(hard: bool, streams: &mut IoStreams) {
};
let l = if hard { rlim_max } else { rlim_cur };
let unit = if resource.resource == RLIMIT_CPU() as c_uint {
let unit = if resource.resource == limits::CPU as c_uint {
"(seconds, "
} else if get_multiplier(resource.resource) == 1 {
"("
@@ -160,7 +237,7 @@ struct Options {
impl Default for Options {
fn default() -> Self {
Options {
what: RLIMIT_FSIZE(),
what: limits::FSIZE,
report_all: false,
hard: false,
soft: false,
@@ -209,26 +286,26 @@ pub fn ulimit(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
'a' => opts.report_all = true,
'H' => opts.hard = true,
'S' => opts.soft = true,
'b' => opts.what = RLIMIT_SBSIZE(),
'c' => opts.what = RLIMIT_CORE(),
'd' => opts.what = RLIMIT_DATA(),
'e' => opts.what = RLIMIT_NICE(),
'f' => opts.what = RLIMIT_FSIZE(),
'i' => opts.what = RLIMIT_SIGPENDING(),
'l' => opts.what = RLIMIT_MEMLOCK(),
'm' => opts.what = RLIMIT_RSS(),
'n' => opts.what = RLIMIT_NOFILE(),
'q' => opts.what = RLIMIT_MSGQUEUE(),
'r' => opts.what = RLIMIT_RTPRIO(),
's' => opts.what = RLIMIT_STACK(),
't' => opts.what = RLIMIT_CPU(),
'u' => opts.what = RLIMIT_NPROC(),
'v' => opts.what = RLIMIT_AS(),
'w' => opts.what = RLIMIT_SWAP(),
'y' => opts.what = RLIMIT_RTTIME(),
'K' => opts.what = RLIMIT_KQUEUES(),
'P' => opts.what = RLIMIT_NPTS(),
'T' => opts.what = RLIMIT_NTHR(),
'b' => opts.what = limits::SBSIZE,
'c' => opts.what = limits::CORE,
'd' => opts.what = limits::DATA,
'e' => opts.what = limits::NICE,
'f' => opts.what = limits::FSIZE,
'i' => opts.what = limits::SIGPENDING,
'l' => opts.what = limits::MEMLOCK,
'm' => opts.what = limits::RSS,
'n' => opts.what = limits::NOFILE,
'q' => opts.what = limits::MSGQUEUE,
'r' => opts.what = limits::RTPRIO,
's' => opts.what = limits::STACK,
't' => opts.what = limits::CPU,
'u' => opts.what = limits::NPROC,
'v' => opts.what = limits::AS,
'w' => opts.what = limits::SWAP,
'y' => opts.what = limits::RTTIME,
'K' => opts.what = limits::KQUEUES,
'P' => opts.what = limits::NPTS,
'T' => opts.what = limits::NTHR,
'h' => {
builtin_print_help(parser, streams, cmd);
return Ok(SUCCESS);
@@ -360,101 +437,96 @@ fn new(
static RESOURCE_ARR: Lazy<Box<[Resource]>> = Lazy::new(|| {
let resources_info = [
(
RLIMIT_SBSIZE(),
limits::SBSIZE,
L!("Maximum size of socket buffers"),
'b',
1024,
),
(
RLIMIT_CORE(),
limits::CORE,
L!("Maximum size of core files created"),
'c',
1024,
),
(
RLIMIT_DATA(),
limits::DATA,
L!("Maximum size of a processs data segment"),
'd',
1024,
),
(limits::NICE, L!("Control of maximum nice priority"), 'e', 1),
(
RLIMIT_NICE(),
L!("Control of maximum nice priority"),
'e',
1,
),
(
RLIMIT_FSIZE(),
limits::FSIZE,
L!("Maximum size of files created by the shell"),
'f',
1024,
),
(
RLIMIT_SIGPENDING(),
limits::SIGPENDING,
L!("Maximum number of pending signals"),
'i',
1,
),
(
RLIMIT_MEMLOCK(),
limits::MEMLOCK,
L!("Maximum size that may be locked into memory"),
'l',
1024,
),
(RLIMIT_RSS(), L!("Maximum resident set size"), 'm', 1024),
(limits::RSS, L!("Maximum resident set size"), 'm', 1024),
(
RLIMIT_NOFILE(),
limits::NOFILE,
L!("Maximum number of open file descriptors"),
'n',
1,
),
(
RLIMIT_MSGQUEUE(),
limits::MSGQUEUE,
L!("Maximum bytes in POSIX message queues"),
'q',
1024,
),
(
RLIMIT_RTPRIO(),
limits::RTPRIO,
L!("Maximum realtime scheduling priority"),
'r',
1,
),
(RLIMIT_STACK(), L!("Maximum stack size"), 's', 1024),
(limits::STACK, L!("Maximum stack size"), 's', 1024),
(
RLIMIT_CPU(),
limits::CPU,
L!("Maximum amount of CPU time in seconds"),
't',
1,
),
(
RLIMIT_NPROC(),
limits::NPROC,
L!("Maximum number of processes available to current user"),
'u',
1,
),
(
RLIMIT_AS(),
limits::AS,
L!("Maximum amount of virtual memory available to each process"),
'v',
1024,
),
(RLIMIT_SWAP(), L!("Maximum swap space"), 'w', 1024),
(limits::SWAP, L!("Maximum swap space"), 'w', 1024),
(
RLIMIT_RTTIME(),
limits::RTTIME,
L!("Maximum contiguous realtime CPU time"),
'y',
1,
),
(RLIMIT_KQUEUES(), L!("Maximum number of kqueues"), 'K', 1),
(limits::KQUEUES, L!("Maximum number of kqueues"), 'K', 1),
(
RLIMIT_NPTS(),
limits::NPTS,
L!("Maximum number of pseudo-terminals"),
'P',
1,
),
(
RLIMIT_NTHR(),
limits::NTHR,
L!("Maximum number of simultaneous threads"),
'T',
1,

View File

@@ -9,14 +9,16 @@
use crate::global_safety::AtomicRef;
use crate::global_safety::RelaxedAtomicBool;
use crate::key;
use crate::libc::MB_CUR_MAX;
use crate::locale::invalidate_numeric_locale;
use crate::parse_util::parse_util_escape_string_with_quote;
use crate::terminal::Output;
use crate::termsize::Termsize;
use crate::wchar::{decode_byte_from_char, encode_byte_to_char, prelude::*};
use crate::wcstringutil::wcs2string_callback;
use crate::wildcard::{ANY_CHAR, ANY_STRING, ANY_STRING_RECURSIVE};
use crate::wutil::encoding::{mbrtowc, wcrtomb, zero_mbstate, AT_LEAST_MB_LEN_MAX};
use crate::wutil::encoding::{
mbrtowc, probe_is_multibyte_locale, wcrtomb, zero_mbstate, AT_LEAST_MB_LEN_MAX,
};
use crate::wutil::fish_iswalnum;
use bitflags::bitflags;
use libc::{SIGTTOU, SIG_IGN, STDIN_FILENO};
@@ -186,7 +188,7 @@ fn escape_string_script(input: &wstr, flags: EscapeFlags) -> WString {
let no_quoted = flags.contains(EscapeFlags::NO_QUOTED);
let no_tilde = flags.contains(EscapeFlags::NO_TILDE);
let no_qmark = feature_test(FeatureFlag::qmark_noglob);
let symbolic = flags.contains(EscapeFlags::SYMBOLIC) && MB_CUR_MAX() > 1;
let symbolic = flags.contains(EscapeFlags::SYMBOLIC) && get_is_multibyte_locale();
assert!(
!symbolic || !escape_printables,
@@ -1060,6 +1062,13 @@ pub fn get_obfuscation_read_char() -> char {
char::from_u32(OBFUSCATION_READ_CHAR.load(Ordering::Relaxed)).unwrap()
}
static IS_MB_LOCALE: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
/// Whether we believe we are in a multibyte locale.
pub fn get_is_multibyte_locale() -> bool {
IS_MB_LOCALE.load()
}
/// Profiling flag. True if commands should be profiled.
pub static PROFILING_ACTIVE: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
@@ -1278,6 +1287,9 @@ pub fn should_suppress_stderr_for_tests() -> bool {
/// This function should be called after calling `setlocale()` to perform fish specific locale
/// initialization.
pub fn fish_setlocale() {
// Invalidate the cached numeric locale.
invalidate_numeric_locale();
// Helper to make a static reference to a static &'wstr, from a string literal.
// This is necessary to store them in global atomics, as these can't handle fat pointers.
macro_rules! LL {
@@ -1287,6 +1299,9 @@ macro_rules! LL {
}};
}
// Mark if we are a multibyte locale.
IS_MB_LOCALE.store(probe_is_multibyte_locale());
// Use various Unicode symbols if they can be encoded using the current locale, else a simple
// ASCII char alternative. All of the can_be_encoded() invocations should return the same
// true/false value since the code points are in the BMP but we're going to be paranoid. This
@@ -1654,7 +1669,7 @@ pub fn get_executable_path(argv0: impl AsRef<Path>) -> PathBuf {
// When /proc/self/exe points to a file that was deleted (or overwritten on update!)
// then linux adds a " (deleted)" suffix.
// If that's not a valid path, let's remove that awkward suffix.
if !path.ends_with(" (deleted)") {
if path.as_os_str().as_bytes().ends_with(b" (deleted)") {
return path;
}

View File

@@ -13,8 +13,7 @@
use crate::flog::FLOG;
use crate::global_safety::RelaxedAtomicBool;
use crate::input::{init_input, FISH_BIND_MODE_VAR};
use crate::libc::{stdout_stream, C_PATH_BSHELL, _PATH_BSHELL};
use crate::nix::{geteuid, getpid, isatty};
use crate::nix::{geteuid, getpid};
use crate::null_terminated_array::OwningNullTerminatedArray;
use crate::path::{
path_emit_config_directory_messages, path_get_cache, path_get_config, path_get_data,
@@ -26,13 +25,11 @@
use crate::wchar::prelude::*;
use crate::wcstringutil::join_strings;
use crate::wutil::{fish_wcstol, wgetcwd, wgettext};
use std::sync::atomic::Ordering;
use libc::{c_int, confstr, uid_t, STDOUT_FILENO, _IONBF};
use libc::{c_int, confstr, uid_t};
use once_cell::sync::{Lazy, OnceCell};
use std::collections::HashMap;
use std::ffi::CStr;
use std::io::Write;
use std::mem::MaybeUninit;
use std::os::unix::prelude::*;
use std::sync::Arc;
@@ -569,13 +566,12 @@ fn setup_user(vars: &EnvStack) {
}
pub(crate) static FALLBACK_PATH: Lazy<&[WString]> = Lazy::new(|| {
use crate::libc::_CS_PATH;
// _CS_PATH: colon-separated paths to find POSIX utilities
let buf_size = unsafe { confstr(_CS_PATH(), std::ptr::null_mut(), 0) };
let buf_size = unsafe { confstr(libc::_CS_PATH, std::ptr::null_mut(), 0) };
Box::leak(
(if buf_size > 0 {
let mut buf = vec![b'\0' as libc::c_char; buf_size];
unsafe { confstr(_CS_PATH(), buf.as_mut_ptr(), buf_size) };
unsafe { confstr(libc::_CS_PATH, buf.as_mut_ptr(), buf_size) };
let buf = buf;
// safety: buf should contain a null-byte, and is not mutable unless we move ownership
let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) };
@@ -858,13 +854,4 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool
/// Various things we need to initialize at run-time that don't really fit any of the other init
/// routines.
pub fn misc_init() {
// If stdout is open on a tty ensure stdio is unbuffered. That's because those functions might
// be intermixed with `write()` calls and we need to ensure the writes are not reordered. See
// issue #3748.
if isatty(STDOUT_FILENO) {
let _ = std::io::stdout().flush();
unsafe { libc::setvbuf(stdout_stream(), std::ptr::null_mut(), _IONBF, 0) };
}
_PATH_BSHELL.store(unsafe { C_PATH_BSHELL().cast_mut() }, Ordering::SeqCst);
}
pub fn misc_init() {}

View File

@@ -14,6 +14,7 @@
use crate::terminal::ColorSupport;
use crate::tty_handoff::xtversion;
use crate::wchar::prelude::*;
use crate::wutil::encoding::probe_is_multibyte_locale;
use crate::wutil::fish_wcstoi;
use crate::{function, terminal};
use std::borrow::Cow;
@@ -452,6 +453,8 @@ fn update_fish_color_support(vars: &EnvStack) {
crate::terminal::set_color_support(color_support);
}
pub const MIDNIGHT_COMMANDER_SID: &wstr = L!("MC_SID");
// Initialize the terminal subsystem
fn init_terminal(vars: &EnvStack) {
let term = vars.get(L!("TERM"));
@@ -464,7 +467,7 @@ fn init_terminal(vars: &EnvStack) {
IS_DUMB.store(term == "dumb");
ONLY_GRAYSCALE.store(term == "ansi-m" || term == "linux-m" || term == "xterm-mono");
if vars.get(L!("MC_SID")).is_some() {
if vars.get(MIDNIGHT_COMMANDER_SID).is_some() {
screen_set_midnight_commander_hack();
}
@@ -549,21 +552,20 @@ fn init_locale(vars: &EnvStack) {
.map(|allow_c| !crate::wcstringutil::bool_from_string(&allow_c))
.unwrap_or(true);
if fix_locale && crate::libc::MB_CUR_MAX() == 1 {
FLOG!(env_locale, "Have singlebyte locale, trying to fix.");
if fix_locale && !probe_is_multibyte_locale() {
FLOG!(env_locale, "Have single byte locale, trying to fix.");
let mut fixed = false;
for locale in UTF8_LOCALES {
{
let locale = CString::new(locale.to_owned()).unwrap();
// this can fail, that is fine
unsafe { libc::setlocale(libc::LC_CTYPE, locale.as_ptr()) };
}
if crate::libc::MB_CUR_MAX() > 1 {
let locale_cstr = CString::new(*locale).unwrap();
// this can fail, that is fine
unsafe { libc::setlocale(libc::LC_CTYPE, locale_cstr.as_ptr()) };
if probe_is_multibyte_locale() {
FLOG!(env_locale, "Fixed locale:", locale);
fixed = true;
break;
}
}
if crate::libc::MB_CUR_MAX() == 1 {
if !fixed {
FLOG!(env_locale, "Failed to fix locale.");
}
}
@@ -573,8 +575,7 @@ fn init_locale(vars: &EnvStack) {
// should never fail, the C locale should always be defined
assert_ne!(loc_ptr, ptr::null_mut());
// See that we regenerate our special locale for numbers
crate::locale::invalidate_numeric_locale();
// Update cached locale information.
crate::common::fish_setlocale();
FLOG!(
env_locale,

View File

@@ -24,12 +24,12 @@
};
#[cfg(FISH_USE_POSIX_SPAWN)]
use crate::fork_exec::spawn::PosixSpawner;
use crate::fork_exec::PATH_BSHELL;
use crate::function::{self, FunctionProperties};
use crate::io::{
BufferedOutputStream, FdOutputStream, IoBufferfill, IoChain, IoClose, IoMode, IoPipe,
IoStreams, OutputStream, SeparatedBuffer, StringOutputStream,
};
use crate::libc::_PATH_BSHELL;
use crate::nix::{getpid, isatty};
use crate::null_terminated_array::OwningNullTerminatedArray;
use crate::parser::{Block, BlockId, BlockType, EvalRes, Parser};
@@ -49,7 +49,7 @@
use crate::wutil::{fish_wcstol, perror};
use errno::{errno, set_errno};
use libc::{
EACCES, ENOENT, ENOEXEC, ENOTDIR, EPIPE, EXIT_FAILURE, EXIT_SUCCESS, STDERR_FILENO,
c_char, EACCES, ENOENT, ENOEXEC, ENOTDIR, EPIPE, EXIT_FAILURE, EXIT_SUCCESS, STDERR_FILENO,
STDIN_FILENO, STDOUT_FILENO,
};
use nix::fcntl::OFlag;
@@ -407,13 +407,14 @@ fn safe_launch_process(
if nargs <= maxargs {
// +1 for /bin/sh, +1 for terminating nullptr
let mut argv2 = [std::ptr::null(); 1 + maxargs + 1];
argv2[0] = _PATH_BSHELL.load(Ordering::Relaxed);
let bshell = PATH_BSHELL.as_ptr() as *const c_char;
argv2[0] = bshell as *mut c_char;
argv2[1..argv.len() + 1].copy_from_slice(argv);
// The command to call should use the full path,
// not what we would pass as argv0.
argv2[1] = actual_cmd.as_ptr();
unsafe {
libc::execve(_PATH_BSHELL.load(Ordering::Relaxed), &argv2[0], envv.get());
libc::execve(bshell, &argv2[0], envv.get());
}
}
}

View File

@@ -22,3 +22,10 @@ pub fn blocked_signals_for_job(job: &Job, sigmask: &mut libc::sigset_t) -> bool
}
false
}
// Bravely define _PATH_BSHELL. On practice it's /bin/sh everywhere, except on Android.
#[cfg(not(target_os = "android"))]
pub static PATH_BSHELL: &[u8] = b"/bin/sh\0";
#[cfg(target_os = "android")]
pub static PATH_BSHELL: &[u8] = b"/system/bin/sh\0";

View File

@@ -1,8 +1,8 @@
//! Wrappers around posix_spawn.
use super::blocked_signals_for_job;
use super::PATH_BSHELL;
use crate::exec::{is_thompson_shell_script, PgroupPolicy};
use crate::libc::_PATH_BSHELL;
use crate::proc::Job;
use crate::redirection::Dup2List;
use crate::signal::signals_to_default;
@@ -10,7 +10,6 @@
use libc::{c_char, posix_spawn_file_actions_t, posix_spawnattr_t};
use std::ffi::{CStr, CString};
use std::mem::MaybeUninit;
use std::sync::atomic::Ordering;
// The posix_spawn family of functions is unusual in that it returns errno codes directly in the return value, not via errno.
// This converts to an error if nonzero.
@@ -173,7 +172,7 @@ pub(crate) fn spawn(
let cmdcstr = unsafe { CStr::from_ptr(cmd) };
if spawn_err.0 == libc::ENOEXEC && is_thompson_shell_script(cmdcstr) {
// Create a new argv with /bin/sh prepended.
let mut argv2 = vec![_PATH_BSHELL.load(Ordering::Relaxed) as *mut c_char];
let mut argv2 = vec![PATH_BSHELL.as_ptr() as *mut c_char];
// The command to call should use the full path,
// not what we would pass as argv0.
@@ -190,7 +189,7 @@ pub(crate) fn spawn(
check_fail(unsafe {
libc::posix_spawn(
&mut pid,
_PATH_BSHELL.load(Ordering::Relaxed),
PATH_BSHELL.as_ptr() as *const c_char,
&self.actions.0,
&self.attr.0,
argv2.as_ptr(),

View File

@@ -8,7 +8,6 @@
VARIABLE_EXPAND, VARIABLE_EXPAND_SINGLE,
};
use crate::expand::{expand_tilde, ExpandFlags, HOME_DIRECTORY};
use crate::libc::_PC_CASE_SENSITIVE;
use crate::operation_context::OperationContext;
use crate::path::path_apply_working_directory;
use crate::redirection::RedirectionMode;
@@ -23,7 +22,6 @@
dir_iter::DirIter, fish_wcstoi, normalize_path, waccess, wbasename, wdirname, wstat,
};
use libc::PATH_MAX;
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::os::fd::RawFd;
@@ -391,27 +389,30 @@ pub fn is_potential_cd_path(
/// false: the filesystem is not case insensitive
/// true: the file system is case insensitive
pub type CaseSensitivityCache = HashMap<WString, bool>;
#[cfg(any(target_os = "macos", target_os = "ios"))]
fn fs_is_case_insensitive(
path: &wstr,
fd: RawFd,
case_sensitivity_cache: &mut CaseSensitivityCache,
) -> bool {
let mut result = false;
if *_PC_CASE_SENSITIVE != 0 {
// Try the cache first.
match case_sensitivity_cache.entry(path.to_owned()) {
Entry::Occupied(e) => {
/* Use the cached value */
result = *e.get();
}
Entry::Vacant(e) => {
// Ask the system. A -1 value means error (so assume case sensitive), a 1 value means case
// sensitive, and a 0 value means case insensitive.
let ret = unsafe { libc::fpathconf(fd, *_PC_CASE_SENSITIVE) };
result = ret == 0;
e.insert(result);
}
}
}
result
if let Some(cached) = case_sensitivity_cache.get(path) {
return *cached;
};
// Ask the system. A -1 value means error (so assume case sensitive), a 1 value means case
// sensitive, and a 0 value means case insensitive.
let ret = unsafe { libc::fpathconf(fd, libc::_PC_CASE_SENSITIVE) };
let icase = ret == 0;
case_sensitivity_cache.insert(path.to_owned(), icase);
icase
}
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
pub fn fs_is_case_insensitive(
_path: &wstr,
_fd: RawFd,
_case_sensitivity_cache: &mut CaseSensitivityCache,
) -> bool {
// Other platforms dont have _PC_CASE_SENSITIVE.
false
}

View File

@@ -1,6 +1,6 @@
use crate::common::{
fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes,
str2wcstring, WSL,
fish_reserved_codepoint, get_is_multibyte_locale, is_windows_subsystem_for_linux, read_blocked,
shell_modes, str2wcstring, WSL,
};
use crate::env::{EnvStack, Environment};
use crate::fd_readable_set::{FdReadableSet, Timeout};
@@ -1699,10 +1699,8 @@ pub(crate) fn decode_input_byte(
use DecodeState::*;
let mut res: char = '\0';
let read_byte = *buffer.last().unwrap();
if crate::libc::MB_CUR_MAX() == 1 {
if !get_is_multibyte_locale() {
// single-byte locale, all values are legal
// FIXME: this looks wrong, this falsely assumes that
// the single-byte locale is compatible with Unicode upper-ASCII.
res = read_byte.into();
out_seq.push(res);
return Complete;

View File

@@ -64,8 +64,6 @@
pub mod job_group;
pub mod key;
pub mod kill;
#[allow(non_snake_case)]
pub mod libc;
pub mod locale;
pub mod nix;
pub mod null_terminated_array;

View File

@@ -1,167 +0,0 @@
#include <locale.h>
#include <paths.h> // _PATH_BSHELL
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h> // MB_CUR_MAX
#include <sys/mount.h> // MNT_LOCAL
#include <sys/resource.h>
#include <sys/statvfs.h> // ST_LOCAL
#include <unistd.h> // _CS_PATH, _PC_CASE_SENSITIVE
size_t C_MB_CUR_MAX() { return MB_CUR_MAX; }
uint64_t C_ST_LOCAL() {
#if defined(ST_LOCAL)
return ST_LOCAL;
#else
return 0;
#endif
}
int C_CS_PATH() {
#if defined(_CS_PATH)
return _CS_PATH;
#else
return -1;
#endif
}
uint64_t C_MNT_LOCAL() {
#if defined(MNT_LOCAL)
return MNT_LOCAL;
#else
return 0;
#endif
}
const char* C_PATH_BSHELL() { return _PATH_BSHELL; }
int C_PC_CASE_SENSITIVE() {
#ifdef _PC_CASE_SENSITIVE
return _PC_CASE_SENSITIVE;
#else
return 0;
#endif
}
FILE* stdout_stream() { return stdout; }
int C_RLIMIT_CORE() { return RLIMIT_CORE; }
int C_RLIMIT_DATA() { return RLIMIT_DATA; }
int C_RLIMIT_FSIZE() { return RLIMIT_FSIZE; }
int C_RLIMIT_NOFILE() { return RLIMIT_NOFILE; }
int C_RLIMIT_STACK() { return RLIMIT_STACK; }
int C_RLIMIT_CPU() { return RLIMIT_CPU; }
int C_RLIMIT_SBSIZE() {
#ifdef RLIMIT_SBSIZE
return RLIMIT_SBSIZE;
#else
return -1;
#endif
}
int C_RLIMIT_NICE() {
#ifdef RLIMIT_NICE
return RLIMIT_NICE;
#else
return -1;
#endif
}
int C_RLIMIT_SIGPENDING() {
#ifdef RLIMIT_SIGPENDING
return RLIMIT_SIGPENDING;
#else
return -1;
#endif
}
int C_RLIMIT_MEMLOCK() {
#ifdef RLIMIT_MEMLOCK
return RLIMIT_MEMLOCK;
#else
return -1;
#endif
}
int C_RLIMIT_RSS() {
#ifdef RLIMIT_RSS
return RLIMIT_RSS;
#else
return -1;
#endif
}
int C_RLIMIT_MSGQUEUE() {
#ifdef RLIMIT_MSGQUEUE
return RLIMIT_MSGQUEUE;
#else
return -1;
#endif
}
int C_RLIMIT_RTPRIO() {
#ifdef RLIMIT_RTPRIO
return RLIMIT_RTPRIO;
#else
return -1;
#endif
}
int C_RLIMIT_NPROC() {
#ifdef RLIMIT_NPROC
return RLIMIT_NPROC;
#else
return -1;
#endif
}
int C_RLIMIT_AS() {
#ifdef RLIMIT_AS
return RLIMIT_AS;
#else
return -1;
#endif
}
int C_RLIMIT_SWAP() {
#ifdef RLIMIT_SWAP
return RLIMIT_SWAP;
#else
return -1;
#endif
}
int C_RLIMIT_RTTIME() {
#ifdef RLIMIT_RTTIME
return RLIMIT_RTTIME;
#else
return -1;
#endif
}
int C_RLIMIT_KQUEUES() {
#ifdef RLIMIT_KQUEUES
return RLIMIT_KQUEUES;
#else
return -1;
#endif
}
int C_RLIMIT_NPTS() {
#ifdef RLIMIT_NPTS
return RLIMIT_NPTS;
#else
return -1;
#endif
}
int C_RLIMIT_NTHR() {
#ifdef RLIMIT_NTHR
return RLIMIT_NTHR;
#else
return -1;
#endif
}

View File

@@ -1,56 +0,0 @@
use std::sync::atomic::AtomicPtr;
use libc::{c_char, c_int};
use once_cell::sync::Lazy;
pub static _PATH_BSHELL: AtomicPtr<c_char> = AtomicPtr::new(std::ptr::null_mut());
extern "C" {
pub fn C_PATH_BSHELL() -> *const c_char;
}
pub static _PC_CASE_SENSITIVE: Lazy<c_int> = Lazy::new(|| unsafe { C_PC_CASE_SENSITIVE() });
extern "C" {
fn C_PC_CASE_SENSITIVE() -> c_int;
}
extern "C" {
pub fn stdout_stream() -> *mut libc::FILE;
pub fn setlinebuf(stream: *mut libc::FILE);
}
macro_rules! CVAR {
($cfn:ident, $cvar:ident, $type:ident) => {
pub fn $cvar() -> $type {
extern "C" {
fn $cfn() -> $type;
}
unsafe { $cfn() }
}
};
}
CVAR!(C_MB_CUR_MAX, MB_CUR_MAX, usize);
CVAR!(C_ST_LOCAL, ST_LOCAL, u64);
CVAR!(C_MNT_LOCAL, MNT_LOCAL, u64);
CVAR!(C_CS_PATH, _CS_PATH, i32);
CVAR!(C_RLIMIT_SBSIZE, RLIMIT_SBSIZE, i32);
CVAR!(C_RLIMIT_CORE, RLIMIT_CORE, i32);
CVAR!(C_RLIMIT_DATA, RLIMIT_DATA, i32);
CVAR!(C_RLIMIT_NICE, RLIMIT_NICE, i32);
CVAR!(C_RLIMIT_FSIZE, RLIMIT_FSIZE, i32);
CVAR!(C_RLIMIT_SIGPENDING, RLIMIT_SIGPENDING, i32);
CVAR!(C_RLIMIT_MEMLOCK, RLIMIT_MEMLOCK, i32);
CVAR!(C_RLIMIT_RSS, RLIMIT_RSS, i32);
CVAR!(C_RLIMIT_NOFILE, RLIMIT_NOFILE, i32);
CVAR!(C_RLIMIT_MSGQUEUE, RLIMIT_MSGQUEUE, i32);
CVAR!(C_RLIMIT_RTPRIO, RLIMIT_RTPRIO, i32);
CVAR!(C_RLIMIT_STACK, RLIMIT_STACK, i32);
CVAR!(C_RLIMIT_CPU, RLIMIT_CPU, i32);
CVAR!(C_RLIMIT_NPROC, RLIMIT_NPROC, i32);
CVAR!(C_RLIMIT_AS, RLIMIT_AS, i32);
CVAR!(C_RLIMIT_SWAP, RLIMIT_SWAP, i32);
CVAR!(C_RLIMIT_RTTIME, RLIMIT_RTTIME, i32);
CVAR!(C_RLIMIT_KQUEUES, RLIMIT_KQUEUES, i32);
CVAR!(C_RLIMIT_NPTS, RLIMIT_NPTS, i32);
CVAR!(C_RLIMIT_NTHR, RLIMIT_NTHR, i32);

View File

@@ -4,14 +4,14 @@
use std::collections::HashMap;
use crate::common::{
escape_string, get_ellipsis_char, get_ellipsis_str, EscapeFlags, EscapeStringStyle,
escape_string, get_ellipsis_char, get_ellipsis_str, get_is_multibyte_locale, EscapeFlags,
EscapeStringStyle,
};
use crate::complete::Completion;
use crate::editable_line::EditableLine;
#[allow(unused_imports)]
use crate::future::IsSomeAnd;
use crate::highlight::{highlight_shell, HighlightRole, HighlightSpec};
use crate::libc::MB_CUR_MAX;
use crate::operation_context::OperationContext;
use crate::screen::{wcswidth_rendered, wcwidth_rendered, CharOffset, Line, ScreenData};
use crate::termsize::Termsize;
@@ -1243,7 +1243,7 @@ fn process_completions_into_infos(lst: &[Completion]) -> Vec<PagerComp> {
// We should probably fix this by first highlighting the original completion, and
// then writing a variant of escape_string() that adjusts highlighting according
// so it matches the escaped string.
&& MB_CUR_MAX() > 1
&& get_is_multibyte_locale()
{
highlight_shell(
&comp.completion,

View File

@@ -694,49 +694,20 @@ pub fn path_remoteness(path: &wstr) -> DirRemoteness {
}
#[cfg(not(any(target_os = "linux", cygwin)))]
{
fn remoteness_via_statfs<StatFS, Flags>(
statfn: unsafe extern "C" fn(*const libc::c_char, *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;
}
let mut buf = MaybeUninit::uninit();
if unsafe { (statfn)(path.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 {
DirRemoteness::local
} else {
DirRemoteness::remote
}
let mut buf = MaybeUninit::uninit();
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)]
let flags = u64::from(buf.f_flags);
if flags & (libc::MNT_LOCAL as u64) != 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
}
}

View File

@@ -54,9 +54,10 @@
use crate::builtins::shared::STATUS_CMD_OK;
use crate::common::ScopeGuarding;
use crate::common::{
escape, escape_string, exit_without_destructors, get_ellipsis_char, get_obfuscation_read_char,
restore_term_foreground_process_group_for_exit, shell_modes, str2wcstring, write_loop,
EscapeFlags, EscapeStringStyle, ScopeGuard, PROGRAM_NAME, UTF8_BOM_WCHAR,
escape, escape_string, exit_without_destructors, get_ellipsis_char, get_is_multibyte_locale,
get_obfuscation_read_char, restore_term_foreground_process_group_for_exit, shell_modes,
str2wcstring, write_loop, EscapeFlags, EscapeStringStyle, ScopeGuard, PROGRAM_NAME,
UTF8_BOM_WCHAR,
};
use crate::complete::{
complete, complete_load, sort_and_prioritize, CompleteFlags, Completion, CompletionList,
@@ -66,6 +67,7 @@
use crate::env::EnvStack;
use crate::env::{EnvMode, Environment, Statuses};
use crate::env_dispatch::guess_emoji_width;
use crate::env_dispatch::MIDNIGHT_COMMANDER_SID;
use crate::exec::exec_subshell;
use crate::expand::expand_one;
use crate::expand::{expand_string, expand_tilde, ExpandFlags, ExpandResultCode};
@@ -96,7 +98,6 @@
use crate::io::IoChain;
use crate::key::ViewportPosition;
use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate};
use crate::libc::MB_CUR_MAX;
use crate::nix::{getpgrp, getpid, isatty};
use crate::operation_context::{get_bg_context, OperationContext};
use crate::pager::{PageRendering, Pager, SelectionMotion};
@@ -260,9 +261,9 @@ fn redirect_tty_after_sighup() {
}
}
fn querying_allowed() -> bool {
fn querying_allowed(vars: &dyn Environment) -> bool {
future_feature_flags::test(FeatureFlag::query_term) &&
!is_dumb() && std::env::var_os("MC_TMPDIR").is_none()
!is_dumb() && vars.get(MIDNIGHT_COMMANDER_SID).is_none()
// Could use /dev/tty in future.
&& isatty(STDOUT_FILENO)
}
@@ -278,10 +279,10 @@ pub fn terminal_init(vars: &dyn Environment, inputfd: RawFd) -> InputEventQueue
);
let _init_tty_metadata = ScopeGuard::new((), |()| {
initialize_tty_protocols();
initialize_tty_protocols(vars);
});
if !querying_allowed() {
if !querying_allowed(vars) {
return input_queue;
}
@@ -1644,7 +1645,7 @@ pub fn combine_command_and_autosuggestion(
impl<'a> Reader<'a> {
pub fn request_cursor_position(&mut self, out: &mut Outputter, q: CursorPositionQuery) {
if !querying_allowed() {
if !querying_allowed(self.vars()) {
return;
}
let mut query = self.blocking_query();
@@ -2629,9 +2630,11 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
ImplicitEvent::QueryInterrupted => (),
ImplicitEvent::FocusIn => {
event::fire_generic(self.parser, L!("fish_focus_in").to_owned(), vec![]);
self.save_screen_state();
}
ImplicitEvent::FocusOut => {
event::fire_generic(self.parser, L!("fish_focus_out").to_owned(), vec![]);
self.save_screen_state();
}
ImplicitEvent::DisableMouseTracking => {
Outputter::stdoutput()
@@ -3223,7 +3226,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
// Update the pager data.
self.pager.set_search_field_shown(true);
self.pager.set_prefix(
if MB_CUR_MAX() > 1 {
if get_is_multibyte_locale() {
L!("")
} else {
L!("> ")

View File

@@ -1,13 +1,15 @@
use std::sync::MutexGuard;
use crate::common::{
escape_string, str2wcstring, unescape_string, wcs2string, EscapeFlags, EscapeStringStyle,
UnescapeStringStyle, ENCODE_DIRECT_BASE, ENCODE_DIRECT_END,
escape_string, fish_setlocale, str2wcstring, unescape_string, wcs2string, EscapeFlags,
EscapeStringStyle, UnescapeStringStyle, ENCODE_DIRECT_BASE, ENCODE_DIRECT_END,
};
use crate::locale::LOCALE_LOCK;
use crate::util::{get_rng_seed, get_seeded_rng};
use crate::wchar::{wstr, WString, L};
use crate::wutil::encoding::{wcrtomb, zero_mbstate, AT_LEAST_MB_LEN_MAX};
use crate::wutil::encoding::{
probe_is_multibyte_locale, wcrtomb, zero_mbstate, AT_LEAST_MB_LEN_MAX,
};
use rand::{Rng, RngCore};
/// wcs2string is locale-dependent, so ensure we have a multibyte locale
@@ -19,13 +21,14 @@ fn setlocale() -> MutexGuard<'static, ()> {
const UTF8_LOCALES: &[&str] = &[
"C.UTF-8", "en_US.UTF-8", "en_GB.UTF-8", "de_DE.UTF-8", "C.utf8", "UTF-8",
];
if crate::libc::MB_CUR_MAX() > 1 {
if probe_is_multibyte_locale() {
return guard;
}
for locale in UTF8_LOCALES {
let locale = std::ffi::CString::new(locale.to_owned()).unwrap();
unsafe { libc::setlocale(libc::LC_CTYPE, locale.as_ptr()) };
if crate::libc::MB_CUR_MAX() > 1 {
if probe_is_multibyte_locale() {
fish_setlocale(); // Update cached locale information.
return guard;
}
}

View File

@@ -2,6 +2,8 @@
//! and reclaiming it after.
use crate::common::{self, safe_write_loop};
use crate::env::Environment;
use crate::env_dispatch::MIDNIGHT_COMMANDER_SID;
use crate::flog::{FLOG, FLOGF};
use crate::global_safety::RelaxedAtomicBool;
use crate::job_group::JobGroup;
@@ -16,7 +18,7 @@
use crate::threads::assert_is_main_thread;
use crate::wchar::prelude::*;
use crate::wchar_ext::ToWString;
use crate::wutil::{perror, wcstoi};
use crate::wutil::{fish_wcstoul, perror, wcstoi};
use libc::{EINVAL, ENOTTY, EPERM, STDIN_FILENO, WNOHANG};
use once_cell::sync::OnceCell;
use std::mem::MaybeUninit;
@@ -60,8 +62,12 @@ pub fn xtversion() -> Option<&'static wstr> {
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum TtyQuirks {
None,
// Running Midnight Commander which can't parse CSI yet.
PreCsiMidnightCommander,
// Running in iTerm2 before 3.5.12, which causes issues when using the kitty keyboard protocol.
PreKittyIterm2,
// Running in Zellij before 0.42.0 which fails to activate the kitty keyboard protocol.
PreKittyZellij,
// Whether we are running under tmux.
Tmux,
// Whether we are running under WezTerm.
@@ -70,10 +76,16 @@ pub enum TtyQuirks {
impl TtyQuirks {
// Create a new TtyQuirks instance with the current environment.
fn detect(xtversion: &wstr) -> Self {
fn detect(vars: &dyn Environment, xtversion: &wstr) -> Self {
use TtyQuirks::*;
if get_iterm2_version(xtversion).is_some_and(|v| v < (3, 5, 12)) {
if vars.get(MIDNIGHT_COMMANDER_SID).is_some()
&& vars.get(L!("__mc_kitty_keyboard")).is_none()
{
PreCsiMidnightCommander
} else if get_iterm2_version(xtversion).is_some_and(|v| v < (3, 5, 12)) {
PreKittyIterm2
} else if get_zellij_version(xtversion).is_some_and(|v| v < 4200) {
PreKittyZellij
} else if xtversion.starts_with(L!("tmux ")) {
Tmux
} else if xtversion.starts_with(L!("WezTerm ")) {
@@ -148,8 +160,11 @@ impl TtyQuirks {
// Determine which keyboard protocol.
// This is used from a signal handler.
fn safe_get_supported_protocol(&self) -> ProtocolKind {
use TtyQuirks::{PreKittyIterm2, Wezterm};
if *self == PreKittyIterm2 {
use TtyQuirks::{PreCsiMidnightCommander, PreKittyIterm2, PreKittyZellij, Wezterm};
if *self == PreCsiMidnightCommander {
return ProtocolKind::None;
}
if matches!(*self, PreKittyIterm2 | PreKittyZellij) {
return ProtocolKind::Other;
}
match KITTY_KEYBOARD_SUPPORTED.get() {
@@ -232,7 +247,7 @@ fn tty_protocols() -> Option<&'static TtyProtocolsSet> {
}
// Initialize serialized commands for enabling/disabling TTY protocols in signal handlers.
pub fn initialize_tty_protocols() {
pub fn initialize_tty_protocols(vars: &dyn Environment) {
// Default missing query responses.
KITTY_KEYBOARD_SUPPORTED.get_or_init(|| false);
SCROLL_CONTENT_UP_SUPPORTED.get_or_init(|| false);
@@ -243,7 +258,7 @@ pub fn initialize_tty_protocols() {
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(TtyQuirks::detect(xtversion).get_protocols()));
p = Box::into_raw(Box::new(TtyQuirks::detect(vars, xtversion).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.
@@ -620,3 +635,8 @@ fn get_iterm2_version(xtversion: &wstr) -> Option<(u32, u32, u32)> {
wcstoi(parts.next()?).ok()?,
))
}
fn get_zellij_version(xtversion: &wstr) -> Option<u64> {
let number = xtversion.strip_prefix("Zellij(")?.strip_suffix(")")?;
fish_wcstoul(number).ok()
}

View File

@@ -297,6 +297,14 @@ fn ends_with<Suffix: IntoCharIter>(&self, suffix: Suffix) -> bool {
)
}
fn strip_suffix<Suffix: IntoCharIter>(&self, suffix: Suffix) -> Option<&wstr> {
// Note the above trait requires a double ended iterator.
let iter = suffix.chars().rev();
let suffix_len = iter.clone().count();
iter_prefixes_iter(iter, self.as_char_slice().iter().rev().copied())
.then(|| self.slice_to(self.char_count() - suffix_len))
}
fn trim_matches(&self, pat: char) -> &wstr {
let slice = self.as_char_slice();
let leading_count = slice.chars().take_while(|&c| c == pat).count();

View File

@@ -1,10 +1,9 @@
//! Helper functions for working with wcstring.
use crate::common::{get_ellipsis_char, get_ellipsis_str};
use crate::common::{get_ellipsis_char, get_ellipsis_str, get_is_multibyte_locale};
use crate::expand::INTERNAL_SEPARATOR;
use crate::fallback::{fish_wcwidth, wcscasecmp, wcscasecmp_fuzzy};
use crate::flog::FLOGF;
use crate::libc::MB_CUR_MAX;
use crate::wchar::{decode_byte_from_char, prelude::*};
use crate::wutil::encoding::{wcrtomb, zero_mbstate, AT_LEAST_MB_LEN_MAX};
@@ -312,7 +311,7 @@ pub fn wcs2string_callback(input: &wstr, mut func: impl FnMut(&[u8]) -> bool) ->
let mut state = zero_mbstate();
let mut converted = [0_u8; AT_LEAST_MB_LEN_MAX];
let is_singlebyte_locale = MB_CUR_MAX() == 1;
let is_singlebyte_locale = !get_is_multibyte_locale();
for c in input.chars() {
// TODO: this doesn't seem sound.

View File

@@ -8,9 +8,43 @@
// HACK This should be mbstate_t from libc but that's not exposed. Since it's only written by
// libc, we define it as opaque type that should be large enough for all implementations.
pub type mbstate_t = [u64; 16];
#[inline]
pub fn zero_mbstate() -> mbstate_t {
[0; 16]
}
// HACK This should be the MB_LEN_MAX macro from libc but that's not easy to get.
pub const AT_LEAST_MB_LEN_MAX: usize = 32;
/// Return true if we believe we are in a multibyte locale.
/// Note this reads the current locale and is modestly expensive - prefer the cached
/// values in `common.rs` which is set by `fish_setlocale`.
pub fn probe_is_multibyte_locale() -> bool {
// In general we would like to read MB_CUR_MAX, but that is not exposed by Rust libc.
// Instead, check if mbrtowc for any byte in the range 0-255 returns (size_t)(-2) which indicates
// the presence of a multibyte locale.
#[inline]
fn is_mb_lead(b: u8) -> bool {
let mut st = zero_mbstate();
let mut wc: libc::wchar_t = 0;
let c = b as libc::c_char;
let n = unsafe {
mbrtowc(
std::ptr::addr_of_mut!(wc).cast::<u32>(),
std::ptr::addr_of!(c),
1,
std::ptr::addr_of_mut!(st),
)
};
n == (-2_i64 as libc::size_t)
}
// Fast path: check common lead bytes.
if is_mb_lead(0xE0) || is_mb_lead(0xC2) {
return true;
}
// Scan non-ASCII high bytes.
(0x80_u8..=0xFF).any(is_mb_lead)
}

View File

@@ -61,7 +61,6 @@ def makeenv(script_path: Path, home: Path) -> Dict[str, str]:
"XDG_DATA_DIRS",
"LANGUAGE",
"MC_SID",
"MC_TMPDIR",
"COLORTERM",
"KONSOLE_VERSION",
"STY",