Compare commits

..

1 Commits

Author SHA1 Message Date
Johannes Altmanninger
03cee61f77 Atom feed of PRs adding/removing translations to/from po/*.po
(This is experimental, and our concrete need to automate this is not
super high, but I want to have a solution for this problem in general.)

There are various automated ways to notify people of changes to a
component, without them having to set up complicated filtering.
For example
1. using GitHub's codeowners feature, perhaps creating a GitHub team
   per component
2. by requiring all changes to the component to go through a fork
3. Using [public-inbox](https://public-inbox.org/)'s "lei" tool,
   which makes it easy to create a filter for all patches touching a given file
   (consumable as pull-based mbox or RSS feed), see
   https://people.kernel.org/monsieuricon/lore-lei-part-1-getting-started

Option 3 seems ideal because it allows anyone to subscribe/unsubscribe,
and it remains obvious where to send PRs to.

It would be fairly easy to write a client-side tool (fetch all commits
and/or PRs and look for interesting changes) but people are more likely
to use something that works OOTB with newsreaders / mail clients.

Expose atom feeds of PRs proposing translation additions/removals.
The one with removals is higher traffic because it can be triggered
when rewording a source string.

Feeds are already online. For example, use:

	https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/zh_CN-additions.atom
	https://raw.githubusercontent.com/fish-shell/fish-shell/fish-shell-events/po/zh_CN-removals.atom

Ref: https://github.com/fish-shell/fish-shell/pull/11833#discussion_r2394161766
2025-10-03 10:26:56 +02:00
43 changed files with 931 additions and 581 deletions

View File

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

View File

@@ -0,0 +1,56 @@
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,26 +1,11 @@
fish ?.?.? (released ???)
=========================
fish 4.1.2 (released ???)
fish 4.1.1 (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,7 +118,6 @@ dependencies = [
"fish-printf",
"libc",
"lru",
"macro_rules_attribute",
"nix",
"num-traits",
"once_cell",
@@ -255,22 +254,6 @@ 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"
@@ -343,12 +326,6 @@ 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,7 +84,6 @@ 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,6 +50,9 @@ 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
@@ -169,7 +172,10 @@ 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;
stack_size <= TWO_MIB
match stack_size {
0..=TWO_MIB => true,
_ => false,
}
}
}

View File

@@ -43,11 +43,6 @@ 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

155
build_tools/event-feeds.py Normal file
View File

@@ -0,0 +1,155 @@
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 --sort-output $rust_extraction_file
msguniq --no-wrap --strict --sort-output $rust_extraction_file
or exit 1
if not set -l --query _flag_use_existing_template

View File

@@ -147,20 +147,14 @@ rm -rf "$tmpdir"
" | sed 's,^\s*| \?,,')"
)
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/$1" \
"$@"
}
# Approve macos-codesign
# TODO what if current user can't approve?
gh_pending_deployments() {
gh_api_repo "actions/runs/$run_id/pending_deployments" "$@"
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" \
"$@"
}
while {
environment_id=$(gh_pending_deployments | jq .[].environment.id)
@@ -176,7 +170,7 @@ echo '
"comment": "Approved via ./build_tools/release.sh"
}
' |
gh_pending_deployments --method POST --input=-
gh_pending_deployments -XPOST --input=-
# Await completion.
gh run watch "$run_id"
@@ -209,7 +203,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 ???)
=========================
@@ -221,25 +215,6 @@ 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,10 +101,14 @@ 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-sections: "
set -g header_prefix "# fish-note: "
function print_header
function print_header -a po_file
set -l ll_CC (basename $po_file .po)
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."
@@ -123,7 +127,7 @@ function merge_po_files --argument-names template_file po_file
or cleanup_exit
begin
print_header
print_header $po_file
# Paste PO file without old header lines.
sed '/^'$header_prefix'/d' $new_po_file
end >$po_file
@@ -139,7 +143,7 @@ for po_file in $po_files
merge_po_files $template_file $po_file
else
begin
print_header
print_header $po_file
cat $template_file
end >$po_file
end

View File

@@ -1,6 +1,9 @@
# 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.
# 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.
# 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,6 +1,9 @@
# 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.
# 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.
# 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,6 +1,9 @@
# 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.
# 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.
# translation of fr.po to Français
# FRENCH TRANSLATION FOR FISH
# Copyright (C) 2006 Xavier Douville

View File

@@ -1,6 +1,9 @@
# 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.
# 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.
# 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,6 +1,9 @@
# 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.
# 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.
# 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,6 +1,9 @@
# 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.
# 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.
# Copyright © 2006
# This file is distributed under the same license as the fish package.
# Axel Liljencrantz <liljencrantz@gmail.com>, 2006

View File

@@ -1,6 +1,9 @@
# 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.
# 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.
#
# General Notices 凡例
# abort: 中止
@@ -3658,7 +3661,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_relative fish-scp)
set -l tmp (__fish_mktemp 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 = None
underline = False
italics = False
dim = False
reverse = False
@@ -263,10 +263,6 @@ 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,7 +24,6 @@
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};
@@ -405,8 +404,8 @@ trait CheckParse: Default {
}
/// Implement the node trait.
macro_rules! Node {
($name:ident) => {
macro_rules! implement_node {
( $name:ident ) => {
impl Node for $name {
fn kind(&self) -> Kind<'_> {
Kind::$name(self)
@@ -426,19 +425,11 @@ 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! Leaf {
($name:ident) => {
macro_rules! implement_leaf {
( $name:ident ) => {
impl Leaf for $name {
fn range(&self) -> Option<SourceRange> {
self.range
@@ -459,20 +450,17 @@ 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, Leaf!)]
#[derive(Default, Debug)]
pub struct $name {
range: Option<SourceRange>,
keyword: ParseKeyword,
}
implement_leaf!($name);
impl Node for $name {
fn kind(&self) -> Kind<'_> {
Kind::Keyword(self)
@@ -501,7 +489,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, Leaf!)]
#[derive(Default, Debug)]
pub struct $name {
range: Option<SourceRange>,
parse_token_type: ParseTokenType,
@@ -514,6 +502,7 @@ 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
@@ -546,9 +535,11 @@ macro_rules! define_list_node {
$name:ident,
$contents:ident
) => {
#[derive(Default, Debug, Node!)]
#[derive(Default, Debug)]
pub struct $name(Box<[$contents]>);
implement_node!($name);
impl Deref for $name {
type Target = Box<[$contents]>;
fn deref(&self) -> &Self::Target {
@@ -591,15 +582,11 @@ fn accept_mut<V: NodeVisitorMut>(&mut self, visitor: &mut V) {
}
/// Implement the acceptor trait for the given branch node.
macro_rules! Acceptor {
macro_rules! implement_acceptor_for_branch {
(
$(#[$_m:meta])*
$_v:vis struct $name:ident {
$(
$(#[$_fm:meta])*
$_fv:vis $field_name:ident : $_ft:ty
),* $(,)?
}
$name:ident
$(, $field_name:ident )*
$(,)?
) => {
impl Acceptor for $name {
#[allow(unused_variables)]
@@ -625,16 +612,18 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
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 {
@@ -644,7 +633,7 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
define_list_node!(VariableAssignmentList, VariableAssignment);
#[derive(Debug, Node!)]
#[derive(Debug)]
pub enum ArgumentOrRedirection {
Argument(Argument),
Redirection(Box<Redirection>), // Boxed because it's bigger
@@ -700,6 +689,8 @@ 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);
@@ -710,7 +701,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, Node!)]
#[derive(Debug)]
pub enum Statement {
Decorated(DecoratedStatement),
Not(Box<NotStatement>),
@@ -719,6 +710,7 @@ pub enum Statement {
If(Box<IfStatement>),
Switch(Box<SwitchStatement>),
}
implement_node!(Statement);
impl Default for Statement {
fn default() -> Self {
@@ -764,7 +756,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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct JobPipeline {
/// Maybe the time keyword.
pub time: Option<KeywordTime>,
@@ -777,9 +769,11 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct JobConjunction {
/// The job conjunction decorator.
pub decorator: Option<JobConjunctionDecorator>,
@@ -792,6 +786,8 @@ 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 {
@@ -806,7 +802,7 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
}
}
#[derive(Default, Debug, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct ForHeader {
/// 'for'
pub kw_for: KeywordFor,
@@ -819,16 +815,20 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct FunctionHeader {
pub kw_function: KeywordFunction,
/// functions require at least one argument.
@@ -836,16 +836,20 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct BlockStatement {
/// A header like for, while, etc.
pub header: BlockStatementHeader,
@@ -856,8 +860,10 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct BraceStatement {
/// The opening brace, in command position.
pub left_brace: TokenLeftBrace,
@@ -868,8 +874,16 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct IfClause {
/// The 'if' keyword.
pub kw_if: KeywordIf,
@@ -880,14 +894,18 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
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
@@ -897,20 +915,22 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
define_list_node!(ElseifClauseList, ElseifClause);
#[derive(Default, Debug, Node!, Acceptor!)]
#[derive(Default, Debug)]
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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct IfStatement {
/// if part
pub if_clause: IfClause,
@@ -923,8 +943,17 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct CaseItem {
/// case \<arguments\> ; body
pub kw_case: KeywordCase,
@@ -932,13 +961,15 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct SwitchStatement {
/// switch \<argument\> ; body ; end args_redirs
pub kw_switch: KeywordSwitch,
@@ -948,10 +979,20 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct DecoratedStatement {
/// An optional decoration (command, builtin, exec, etc).
pub opt_decoration: Option<DecoratedStatementDecorator>,
@@ -960,9 +1001,11 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct NotStatement {
/// Keyword, either not or exclam.
pub kw: KeywordNot,
@@ -970,14 +1013,18 @@ pub struct NotStatement {
pub variables: VariableAssignmentList,
pub contents: Statement,
}
implement_node!(NotStatement);
implement_acceptor_for_branch!(NotStatement, kw, time, variables, contents);
#[derive(Default, Debug, Node!, Acceptor!)]
#[derive(Default, Debug)]
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
@@ -986,7 +1033,7 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
define_list_node!(JobContinuationList, JobContinuation);
#[derive(Default, Debug, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct JobConjunctionContinuation {
/// The && or || token.
pub conjunction: TokenConjunction,
@@ -994,6 +1041,8 @@ 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);
@@ -1004,10 +1053,12 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
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;
@@ -1029,10 +1080,12 @@ 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, Node!, Acceptor!)]
#[derive(Default, Debug)]
pub struct FreestandingArgumentList {
pub arguments: ArgumentList,
}
implement_node!(FreestandingArgumentList);
implement_acceptor_for_branch!(FreestandingArgumentList, arguments);
define_list_node!(JobConjunctionContinuationList, JobConjunctionContinuation);
@@ -1044,10 +1097,12 @@ pub struct FreestandingArgumentList {
define_list_node!(CaseItemList, CaseItem);
/// A variable_assignment contains a source range like FOO=bar.
#[derive(Default, Debug, Node!, Leaf!)]
#[derive(Default, Debug)]
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?
@@ -1069,17 +1124,21 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
}
/// Zero or more newlines.
#[derive(Default, Debug, Node!, Leaf!)]
#[derive(Default, Debug)]
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, Node!, Leaf!)]
#[derive(Default, Debug)]
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
@@ -1167,13 +1226,14 @@ pub fn decoration(&self) -> StatementDecoration {
}
}
#[derive(Debug, Node!)]
#[derive(Debug)]
pub enum BlockStatementHeader {
Begin(BeginHeader),
For(ForHeader),
While(WhileHeader),
Function(FunctionHeader),
}
implement_node!(BlockStatementHeader);
impl Default for BlockStatementHeader {
fn default() -> Self {

View File

@@ -246,18 +246,20 @@ 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 any.
/// `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 HAVE_FIELD_WIDTH and HAVE_PRECISION are true, respectively.
/// ARGUMENT is the argument to be formatted.
#[allow(clippy::collapsible_else_if, clippy::too_many_arguments)]
fn print_directive(
fn print_direc(
&mut self,
spec: &wstr,
conversion: char,
field_width: Option<i64>,
precision: Option<i64>,
have_field_width: bool,
field_width: i32,
have_precision: bool,
precision: i32,
argument: &wstr,
) {
/// Printf macro helper which provides our locale.
@@ -281,6 +283,21 @@ 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);
@@ -289,96 +306,77 @@ macro_rules! append_output_fmt {
match conversion {
'd' | 'i' => {
let arg: i64 = string_to_scalar_type(argument, self);
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);
}
},
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, arg);
} else {
append_output_fmt!(fmt, precision, arg);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, arg);
} else {
append_output_fmt!(fmt, field_width, precision, arg);
}
}
}
'o' | 'u' | 'x' | 'X' => {
let arg: u64 = string_to_scalar_type(argument, self);
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);
}
},
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, arg);
} else {
append_output_fmt!(fmt, precision, arg);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, arg);
} else {
append_output_fmt!(fmt, field_width, precision, arg);
}
}
}
'a' | 'A' | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' => {
let arg: f64 = string_to_scalar_type(argument, self);
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);
}
},
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, arg);
} else {
append_output_fmt!(fmt, precision, arg);
}
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, arg);
} else {
append_output_fmt!(fmt, field_width, precision, arg);
}
}
}
'c' => match field_width {
Some(field_width) => {
'c' => {
if !have_field_width {
append_output_fmt!(fmt, argument.char_at(0));
} else {
append_output_fmt!(fmt, field_width, argument.char_at(0));
}
None => {
append_output_fmt!(fmt, argument.char_at(0));
}
},
}
'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) => {
's' => {
if !have_field_width {
if !have_precision {
append_output_fmt!(fmt, argument);
} else {
append_output_fmt!(fmt, precision, argument);
}
None => {
append_output_fmt!(fmt, argument);
} else {
if !have_precision {
append_output_fmt!(fmt, field_width, argument);
} else {
append_output_fmt!(fmt, field_width, precision, argument);
}
},
},
}
}
_ => {
panic!("unexpected opt: {}", conversion);
@@ -386,16 +384,18 @@ 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 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 direc_start: &wstr; /* Start of % directive. */
let mut direc_length: usize; /* Length of % directive. */
let mut have_field_width: bool; /* True if FIELD_WIDTH is valid. */
let mut field_width: c_int = 0; /* Arg to first '*'. */
let mut have_precision: bool; /* True if PRECISION is valid. */
let mut precision = 0; /* Arg to second '*'. */
let mut ok = [false; 256]; /* ok['x'] is true if %x is allowed. */
// N.B. this was originally written as a loop like so:
@@ -414,11 +414,11 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
match f.char_at(0) {
'%' => {
directive_start = f;
direc_start = f;
f = &f[1..];
directive_length = 1;
field_width = None;
precision = None;
direc_length = 1;
have_field_width = false;
have_precision = false;
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..];
directive_length += 1;
direc_length += 1;
}
}
if f.char_at(0) == '*' {
f = &f[1..];
directive_length += 1;
direc_length += 1;
if argc > 0 {
let width: i64 = string_to_scalar_type(argv[0], self);
if (c_int::MIN as i64) <= width && width <= (c_int::MAX as i64) {
field_width = Some(width);
field_width = width as c_int;
} else {
self.fatal_error(wgettext_fmt!(
"invalid field width: %ls",
@@ -480,45 +480,47 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
argv = &argv[1..];
argc -= 1;
} else {
field_width = Some(0);
field_width = 0;
}
have_field_width = true;
} else {
while iswdigit(f.char_at(0)) {
f = &f[1..];
directive_length += 1;
direc_length += 1;
}
}
if f.char_at(0) == '.' {
f = &f[1..];
directive_length += 1;
direc_length += 1;
modify_allowed_format_specifiers(&mut ok, "c", false);
if f.char_at(0) == '*' {
f = &f[1..];
directive_length += 1;
direc_length += 1;
if argc > 0 {
let prec: i64 = string_to_scalar_type(argv[0], self);
if prec < 0 {
// A negative precision is taken as if the precision were omitted,
// so -1 is safe here even if prec < INT_MIN.
precision = Some(-1);
precision = -1;
} else if (c_int::MAX as i64) < prec {
self.fatal_error(wgettext_fmt!(
"invalid precision: %ls",
argv[0]
));
} else {
precision = Some(prec);
precision = prec as c_int;
}
argv = &argv[1..];
argc -= 1;
} else {
precision = Some(0);
precision = 0;
}
have_precision = true;
} else {
while iswdigit(f.char_at(0)) {
f = &f[1..];
directive_length += 1;
direc_length += 1;
}
}
}
@@ -531,8 +533,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, directive_start) + 1,
directive_start
wstr_offset_in(f, direc_start) + 1,
direc_start
));
return 0;
}
@@ -543,10 +545,12 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
argv = &argv[1..];
argc -= 1;
}
self.print_directive(
&directive_start[..directive_length],
self.print_direc(
&direc_start[..direc_length],
f.char_at(0),
have_field_width,
field_width,
have_precision,
precision,
argument,
);

View File

@@ -5,88 +5,11 @@
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();
@@ -134,7 +57,7 @@ fn print_all(hard: bool, streams: &mut IoStreams) {
};
let l = if hard { rlim_max } else { rlim_cur };
let unit = if resource.resource == limits::CPU as c_uint {
let unit = if resource.resource == RLIMIT_CPU() as c_uint {
"(seconds, "
} else if get_multiplier(resource.resource) == 1 {
"("
@@ -237,7 +160,7 @@ struct Options {
impl Default for Options {
fn default() -> Self {
Options {
what: limits::FSIZE,
what: RLIMIT_FSIZE(),
report_all: false,
hard: false,
soft: false,
@@ -286,26 +209,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 = 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,
'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(),
'h' => {
builtin_print_help(parser, streams, cmd);
return Ok(SUCCESS);
@@ -437,96 +360,101 @@ fn new(
static RESOURCE_ARR: Lazy<Box<[Resource]>> = Lazy::new(|| {
let resources_info = [
(
limits::SBSIZE,
RLIMIT_SBSIZE(),
L!("Maximum size of socket buffers"),
'b',
1024,
),
(
limits::CORE,
RLIMIT_CORE(),
L!("Maximum size of core files created"),
'c',
1024,
),
(
limits::DATA,
RLIMIT_DATA(),
L!("Maximum size of a processs data segment"),
'd',
1024,
),
(limits::NICE, L!("Control of maximum nice priority"), 'e', 1),
(
limits::FSIZE,
RLIMIT_NICE(),
L!("Control of maximum nice priority"),
'e',
1,
),
(
RLIMIT_FSIZE(),
L!("Maximum size of files created by the shell"),
'f',
1024,
),
(
limits::SIGPENDING,
RLIMIT_SIGPENDING(),
L!("Maximum number of pending signals"),
'i',
1,
),
(
limits::MEMLOCK,
RLIMIT_MEMLOCK(),
L!("Maximum size that may be locked into memory"),
'l',
1024,
),
(limits::RSS, L!("Maximum resident set size"), 'm', 1024),
(RLIMIT_RSS(), L!("Maximum resident set size"), 'm', 1024),
(
limits::NOFILE,
RLIMIT_NOFILE(),
L!("Maximum number of open file descriptors"),
'n',
1,
),
(
limits::MSGQUEUE,
RLIMIT_MSGQUEUE(),
L!("Maximum bytes in POSIX message queues"),
'q',
1024,
),
(
limits::RTPRIO,
RLIMIT_RTPRIO(),
L!("Maximum realtime scheduling priority"),
'r',
1,
),
(limits::STACK, L!("Maximum stack size"), 's', 1024),
(RLIMIT_STACK(), L!("Maximum stack size"), 's', 1024),
(
limits::CPU,
RLIMIT_CPU(),
L!("Maximum amount of CPU time in seconds"),
't',
1,
),
(
limits::NPROC,
RLIMIT_NPROC(),
L!("Maximum number of processes available to current user"),
'u',
1,
),
(
limits::AS,
RLIMIT_AS(),
L!("Maximum amount of virtual memory available to each process"),
'v',
1024,
),
(limits::SWAP, L!("Maximum swap space"), 'w', 1024),
(RLIMIT_SWAP(), L!("Maximum swap space"), 'w', 1024),
(
limits::RTTIME,
RLIMIT_RTTIME(),
L!("Maximum contiguous realtime CPU time"),
'y',
1,
),
(limits::KQUEUES, L!("Maximum number of kqueues"), 'K', 1),
(RLIMIT_KQUEUES(), L!("Maximum number of kqueues"), 'K', 1),
(
limits::NPTS,
RLIMIT_NPTS(),
L!("Maximum number of pseudo-terminals"),
'P',
1,
),
(
limits::NTHR,
RLIMIT_NTHR(),
L!("Maximum number of simultaneous threads"),
'T',
1,

View File

@@ -9,16 +9,14 @@
use crate::global_safety::AtomicRef;
use crate::global_safety::RelaxedAtomicBool;
use crate::key;
use crate::locale::invalidate_numeric_locale;
use crate::libc::MB_CUR_MAX;
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, probe_is_multibyte_locale, wcrtomb, zero_mbstate, AT_LEAST_MB_LEN_MAX,
};
use crate::wutil::encoding::{mbrtowc, wcrtomb, zero_mbstate, AT_LEAST_MB_LEN_MAX};
use crate::wutil::fish_iswalnum;
use bitflags::bitflags;
use libc::{SIGTTOU, SIG_IGN, STDIN_FILENO};
@@ -188,7 +186,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) && get_is_multibyte_locale();
let symbolic = flags.contains(EscapeFlags::SYMBOLIC) && MB_CUR_MAX() > 1;
assert!(
!symbolic || !escape_printables,
@@ -1062,13 +1060,6 @@ 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);
@@ -1287,9 +1278,6 @@ 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 {
@@ -1299,9 +1287,6 @@ 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
@@ -1669,7 +1654,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.as_os_str().as_bytes().ends_with(b" (deleted)") {
if !path.ends_with(" (deleted)") {
return path;
}

View File

@@ -13,7 +13,8 @@
use crate::flog::FLOG;
use crate::global_safety::RelaxedAtomicBool;
use crate::input::{init_input, FISH_BIND_MODE_VAR};
use crate::nix::{geteuid, getpid};
use crate::libc::{stdout_stream, C_PATH_BSHELL, _PATH_BSHELL};
use crate::nix::{geteuid, getpid, isatty};
use crate::null_terminated_array::OwningNullTerminatedArray;
use crate::path::{
path_emit_config_directory_messages, path_get_cache, path_get_config, path_get_data,
@@ -25,11 +26,13 @@
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};
use libc::{c_int, confstr, uid_t, STDOUT_FILENO, _IONBF};
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;
@@ -566,12 +569,13 @@ 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(libc::_CS_PATH, std::ptr::null_mut(), 0) };
let buf_size = unsafe { confstr(_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(libc::_CS_PATH, buf.as_mut_ptr(), buf_size) };
unsafe { confstr(_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()) };
@@ -854,4 +858,13 @@ 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() {}
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);
}

View File

@@ -14,7 +14,6 @@
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;
@@ -453,8 +452,6 @@ 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"));
@@ -467,7 +464,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(MIDNIGHT_COMMANDER_SID).is_some() {
if vars.get(L!("MC_SID")).is_some() {
screen_set_midnight_commander_hack();
}
@@ -552,20 +549,21 @@ fn init_locale(vars: &EnvStack) {
.map(|allow_c| !crate::wcstringutil::bool_from_string(&allow_c))
.unwrap_or(true);
if fix_locale && !probe_is_multibyte_locale() {
FLOG!(env_locale, "Have single byte locale, trying to fix.");
let mut fixed = false;
if fix_locale && crate::libc::MB_CUR_MAX() == 1 {
FLOG!(env_locale, "Have singlebyte locale, trying to fix.");
for locale in UTF8_LOCALES {
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() {
{
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 {
FLOG!(env_locale, "Fixed locale:", locale);
fixed = true;
break;
}
}
if !fixed {
if crate::libc::MB_CUR_MAX() == 1 {
FLOG!(env_locale, "Failed to fix locale.");
}
}
@@ -575,7 +573,8 @@ fn init_locale(vars: &EnvStack) {
// should never fail, the C locale should always be defined
assert_ne!(loc_ptr, ptr::null_mut());
// Update cached locale information.
// See that we regenerate our special locale for numbers
crate::locale::invalidate_numeric_locale();
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::{
c_char, EACCES, ENOENT, ENOEXEC, ENOTDIR, EPIPE, EXIT_FAILURE, EXIT_SUCCESS, STDERR_FILENO,
EACCES, ENOENT, ENOEXEC, ENOTDIR, EPIPE, EXIT_FAILURE, EXIT_SUCCESS, STDERR_FILENO,
STDIN_FILENO, STDOUT_FILENO,
};
use nix::fcntl::OFlag;
@@ -407,14 +407,13 @@ fn safe_launch_process(
if nargs <= maxargs {
// +1 for /bin/sh, +1 for terminating nullptr
let mut argv2 = [std::ptr::null(); 1 + maxargs + 1];
let bshell = PATH_BSHELL.as_ptr() as *const c_char;
argv2[0] = bshell as *mut c_char;
argv2[0] = _PATH_BSHELL.load(Ordering::Relaxed);
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(bshell, &argv2[0], envv.get());
libc::execve(_PATH_BSHELL.load(Ordering::Relaxed), &argv2[0], envv.get());
}
}
}

View File

@@ -22,10 +22,3 @@ 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,6 +10,7 @@
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.
@@ -172,7 +173,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.as_ptr() as *mut c_char];
let mut argv2 = vec![_PATH_BSHELL.load(Ordering::Relaxed) as *mut c_char];
// The command to call should use the full path,
// not what we would pass as argv0.
@@ -189,7 +190,7 @@ pub(crate) fn spawn(
check_fail(unsafe {
libc::posix_spawn(
&mut pid,
PATH_BSHELL.as_ptr() as *const c_char,
_PATH_BSHELL.load(Ordering::Relaxed),
&self.actions.0,
&self.attr.0,
argv2.as_ptr(),

View File

@@ -8,6 +8,7 @@
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;
@@ -22,6 +23,7 @@
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;
@@ -389,30 +391,27 @@ 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 {
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
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
}

View File

@@ -1,6 +1,6 @@
use crate::common::{
fish_reserved_codepoint, get_is_multibyte_locale, is_windows_subsystem_for_linux, read_blocked,
shell_modes, str2wcstring, WSL,
fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes,
str2wcstring, WSL,
};
use crate::env::{EnvStack, Environment};
use crate::fd_readable_set::{FdReadableSet, Timeout};
@@ -1699,8 +1699,10 @@ pub(crate) fn decode_input_byte(
use DecodeState::*;
let mut res: char = '\0';
let read_byte = *buffer.last().unwrap();
if !get_is_multibyte_locale() {
if crate::libc::MB_CUR_MAX() == 1 {
// 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,6 +64,8 @@
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;

167
src/libc.c Normal file
View File

@@ -0,0 +1,167 @@
#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
}

56
src/libc.rs Normal file
View File

@@ -0,0 +1,56 @@
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, get_is_multibyte_locale, EscapeFlags,
EscapeStringStyle,
escape_string, get_ellipsis_char, get_ellipsis_str, 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.
&& get_is_multibyte_locale()
&& MB_CUR_MAX() > 1
{
highlight_shell(
&comp.completion,

View File

@@ -694,20 +694,49 @@ pub fn path_remoteness(path: &wstr) -> DirRemoteness {
}
#[cfg(not(any(target_os = "linux", cygwin)))]
{
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
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
}
}
// 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,10 +54,9 @@
use crate::builtins::shared::STATUS_CMD_OK;
use crate::common::ScopeGuarding;
use crate::common::{
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,
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,
};
use crate::complete::{
complete, complete_load, sort_and_prioritize, CompleteFlags, Completion, CompletionList,
@@ -67,7 +66,6 @@
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};
@@ -98,6 +96,7 @@
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};
@@ -261,9 +260,9 @@ fn redirect_tty_after_sighup() {
}
}
fn querying_allowed(vars: &dyn Environment) -> bool {
fn querying_allowed() -> bool {
future_feature_flags::test(FeatureFlag::query_term) &&
!is_dumb() && vars.get(MIDNIGHT_COMMANDER_SID).is_none()
!is_dumb() && std::env::var_os("MC_TMPDIR").is_none()
// Could use /dev/tty in future.
&& isatty(STDOUT_FILENO)
}
@@ -279,10 +278,10 @@ pub fn terminal_init(vars: &dyn Environment, inputfd: RawFd) -> InputEventQueue
);
let _init_tty_metadata = ScopeGuard::new((), |()| {
initialize_tty_protocols(vars);
initialize_tty_protocols();
});
if !querying_allowed(vars) {
if !querying_allowed() {
return input_queue;
}
@@ -1645,7 +1644,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(self.vars()) {
if !querying_allowed() {
return;
}
let mut query = self.blocking_query();
@@ -2630,11 +2629,9 @@ 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()
@@ -3226,7 +3223,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 get_is_multibyte_locale() {
if MB_CUR_MAX() > 1 {
L!("")
} else {
L!("> ")

View File

@@ -1,15 +1,13 @@
use std::sync::MutexGuard;
use crate::common::{
escape_string, fish_setlocale, str2wcstring, unescape_string, wcs2string, EscapeFlags,
EscapeStringStyle, UnescapeStringStyle, ENCODE_DIRECT_BASE, ENCODE_DIRECT_END,
escape_string, 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::{
probe_is_multibyte_locale, wcrtomb, zero_mbstate, AT_LEAST_MB_LEN_MAX,
};
use crate::wutil::encoding::{wcrtomb, zero_mbstate, AT_LEAST_MB_LEN_MAX};
use rand::{Rng, RngCore};
/// wcs2string is locale-dependent, so ensure we have a multibyte locale
@@ -21,14 +19,13 @@ 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 probe_is_multibyte_locale() {
if crate::libc::MB_CUR_MAX() > 1 {
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 probe_is_multibyte_locale() {
fish_setlocale(); // Update cached locale information.
if crate::libc::MB_CUR_MAX() > 1 {
return guard;
}
}

View File

@@ -2,8 +2,6 @@
//! 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;
@@ -18,7 +16,7 @@
use crate::threads::assert_is_main_thread;
use crate::wchar::prelude::*;
use crate::wchar_ext::ToWString;
use crate::wutil::{fish_wcstoul, perror, wcstoi};
use crate::wutil::{perror, wcstoi};
use libc::{EINVAL, ENOTTY, EPERM, STDIN_FILENO, WNOHANG};
use once_cell::sync::OnceCell;
use std::mem::MaybeUninit;
@@ -62,12 +60,8 @@ 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.
@@ -76,16 +70,10 @@ pub enum TtyQuirks {
impl TtyQuirks {
// Create a new TtyQuirks instance with the current environment.
fn detect(vars: &dyn Environment, xtversion: &wstr) -> Self {
fn detect(xtversion: &wstr) -> Self {
use TtyQuirks::*;
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)) {
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 ")) {
@@ -160,11 +148,8 @@ impl TtyQuirks {
// Determine which keyboard protocol.
// This is used from a signal handler.
fn safe_get_supported_protocol(&self) -> ProtocolKind {
use TtyQuirks::{PreCsiMidnightCommander, PreKittyIterm2, PreKittyZellij, Wezterm};
if *self == PreCsiMidnightCommander {
return ProtocolKind::None;
}
if matches!(*self, PreKittyIterm2 | PreKittyZellij) {
use TtyQuirks::{PreKittyIterm2, Wezterm};
if *self == PreKittyIterm2 {
return ProtocolKind::Other;
}
match KITTY_KEYBOARD_SUPPORTED.get() {
@@ -247,7 +232,7 @@ fn tty_protocols() -> Option<&'static TtyProtocolsSet> {
}
// Initialize serialized commands for enabling/disabling TTY protocols in signal handlers.
pub fn initialize_tty_protocols(vars: &dyn Environment) {
pub fn initialize_tty_protocols() {
// Default missing query responses.
KITTY_KEYBOARD_SUPPORTED.get_or_init(|| false);
SCROLL_CONTENT_UP_SUPPORTED.get_or_init(|| false);
@@ -258,7 +243,7 @@ pub fn initialize_tty_protocols(vars: &dyn Environment) {
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(vars, xtversion).get_protocols()));
p = Box::into_raw(Box::new(TtyQuirks::detect(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.
@@ -635,8 +620,3 @@ 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,14 +297,6 @@ 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,9 +1,10 @@
//! Helper functions for working with wcstring.
use crate::common::{get_ellipsis_char, get_ellipsis_str, get_is_multibyte_locale};
use crate::common::{get_ellipsis_char, get_ellipsis_str};
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};
@@ -311,7 +312,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 = !get_is_multibyte_locale();
let is_singlebyte_locale = MB_CUR_MAX() == 1;
for c in input.chars() {
// TODO: this doesn't seem sound.

View File

@@ -8,43 +8,9 @@
// 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,6 +61,7 @@ def makeenv(script_path: Path, home: Path) -> Dict[str, str]:
"XDG_DATA_DIRS",
"LANGUAGE",
"MC_SID",
"MC_TMPDIR",
"COLORTERM",
"KONSOLE_VERSION",
"STY",