Compare commits

...

104 Commits

Author SHA1 Message Date
Johannes Altmanninger
56bbdb3f39 Release 4.0.9
Created by ./build_tools/release.sh 4.0.9
2025-09-27 20:24:24 +02:00
Johannes Altmanninger
ce4aa7669d Release workflow fixups 2025-09-27 20:20:44 +02:00
Johannes Altmanninger
af8d8d3d1b release-notes.sh: add stats, round off committer list
Instead of adding these to the Markdown directly, add it to the
fake CHANGELOG.rst source, which makes escaping easier, and allows
generating other formats than Markdown in future.

(cherry picked from commit 06bbac8ed6)
2025-09-27 14:23:25 +02:00
Johannes Altmanninger
fecd0b4bf1 Revert "Only load sphinx_markdown_builder extension if it's used"
sphinx-build fails with

	sphinx.errors.SphinxError: Builder name markdown not registered or available through entry point

Apparently this issue was hidden locally by caching, and not checked
in CI because of this error causing
tests/checks/sphinx-markdown-changelog.fish to be skipped.

	sphinx-build 7.2.6
	runner@runnervm3ublj:~/work/fish-shell/fish-shell$ python -c 'import sphinx_markdown_builder'
	Traceback (most recent call last):
	  File "<string>", line 1, in <module>
	  File "/home/runner/.local/lib/python3.12/site-packages/sphinx_markdown_builder/__init__.py", line 6, in <module>
	    from sphinx.util.typing import ExtensionMetadata
	ImportError: cannot import name 'ExtensionMetadata' from 'sphinx.util.typing' (/usr/lib/python3/dist-packages/sphinx/util/typing.py)

This reverts commit 7b495497d7.

While at it, fail the test earlier if something went wrong, because the
remaining check will likely also fail and confuse.

(cherry picked from commit aab22a453b)
2025-09-27 14:23:25 +02:00
Johannes Altmanninger
2d4a43302a release-notes.sh: remove line breaks from generated Markdown, for GitHub
GitHub-flavored Markdown translates line breaks to <br/>, which does
not match our intent. Work around that by joining lines when producing
Markdown output.

(cherry picked from commit 4cc2d2ec30)
2025-09-27 14:23:25 +02:00
Johannes Altmanninger
5a4a913220 Enable sphinx parallelism also when building markdown release notes
We get a warning about sphinx_markdown_builder not being
parallelizable. Fix that.

Ref: https://github.com/liran-funaro/sphinx-markdown-builder/pull/38
(cherry picked from commit e6541c5c93)
2025-09-27 06:03:16 +02:00
Johannes Altmanninger
904ceba858 Only load sphinx_markdown_builder extension if it's used
As is, building man pages or HTML docs while sphinx_markdown_builder
is installed, will result in unrelated warnings.  Remove those by
removing it from the extensions config.  Markdown building (only used
for changelog) seems to work without this just fine.

(cherry picked from commit 7b495497d7)
2025-09-27 06:03:16 +02:00
Johannes Altmanninger
3c2adfbd4b Fix markdown changelog generation test
System tests typically run outside the workspace directory, but they
still have read-only access to the workspace; fix it accordingly.
This test only works on git checkouts, not in tarballs, so skip it
if .git doesn't exist.

(cherry picked from commit 6e90d9bd6f)
2025-09-27 06:03:16 +02:00
Johannes Altmanninger
9e4850b40a Disable sphinx-markdown-builder in tests again for now
Somehow I didn't realize this breaks the tests/checks/sphinx-* tests.

(cherry picked from commit b37b57781b)
2025-09-27 06:03:16 +02:00
Johannes Altmanninger
211b3f6670 release workflow: credit contributors in release notes
While at it, do a 's/^--$/^---/' to fix Markdown syntax for horizontal
line for CommonMark-based parsers.

(cherry picked from commit 22ffc31b71)
2025-09-24 15:58:42 +02:00
Johannes Altmanninger
d530e127f5 Test markdown changelog creation in CI
Extract a github action to install the same version used in the release
workflow.  In future we should probably migrate to requirements.txt
or similar.

(cherry picked from commit 127c02992d)
2025-09-24 15:58:42 +02:00
Johannes Altmanninger
a6698098db release workflow: resolve relative references in changelog properly
Instead of having sphinx-build only build CHANGELOG.rst, build the
entire thing, so relative references (":doc:", ":ref:") can be resolved
properly.  Replace our sed-based hacks with 'markdown_http_base'.

(cherry picked from commit 765ca54d59)
2025-09-24 15:58:42 +02:00
Johannes Altmanninger
f73b260a3a release workflow: extract script for generating markdown release notes
(cherry picked from commit 1519ea74be)
2025-09-24 15:58:42 +02:00
Daniel Rainer
ffa7abd6ff Fix release workflow syntax
The previous version results in an immediate workflow failure due to a
syntax error in the YAML. `workflow_dispatch` should be a dictionary
key, with its value being another dictionary (whose only key is `inputs`
at the moment).

(cherry picked from commit c8f31ceedb)
2025-09-24 15:58:42 +02:00
Johannes Altmanninger
b87ef689fa github release workflow: fix structure
(cherry picked from commit b1d1ef1b6e)
2025-09-24 15:58:42 +02:00
Johannes Altmanninger
4fe70f6965 github release workflow: only run on explicit dispatch
Release automation can be tested on any GitHub fork, using

	build_tools/release.sh $version $repository_owner $git_remote

which should work perfectly except for macOS packages (which fail
unless provided GitHub secrets).

People might push tags to their forks, both non-release tags (which
would trigger an early failure in "is-release-tag") or replicas of
our actual release tags (which would create a draft release etc. and
only fail when building macOS packages).

Run on explicit workflow dispatch to make sure it's not triggered by
accident like that.

This means that we'll use the .github/workflows/release.yml from
the default branch (i.e. master), so try to make sure it matches the
version in the release, to prevent accidents.

Closes #11816

(cherry picked from commit 01361b9217)
2025-09-24 15:58:42 +02:00
Johannes Altmanninger
7001abca9f github release workflow: use github context only if needed
(cherry picked from commit dab8df1a18)
2025-09-24 15:58:42 +02:00
Johannes Altmanninger
199316f1a3 build_tools/make_macos_pkg.sh: fix inconsistent version computation
make_tarball.sh and others do it differently.

(cherry picked from commit c771ff06d4)
2025-09-24 15:58:42 +02:00
David Adam
78c9ab29cd debian packaging: don't remove Cargo.toml.orig files
Newer versions of cargo include the Cargo.toml.orig file when vendoring,
but dh_clean removes those by default. Try to disable this to fix the
package builds again.

(cherry picked from commit 67e8657109)
2025-09-21 10:41:44 +08:00
David Adam
91c9dbdd89 debian packaging: use the correct test target
(cherry picked from commit 5e2ddaace9)
2025-09-21 10:41:44 +08:00
David Adam
e737ad1f0f debian packaging: fix typo 2025-09-20 22:11:10 +08:00
Johannes Altmanninger
6f536c6304 Fix new job control test; changelog 2025-09-20 14:57:49 +02:00
Piotr Kubaj
a1b4b391b2 path.rs: fix build on ARM / POWER
error[E0308]: mismatched types
   --> src/path.rs:749:13
    |
748 |         let remoteness = remoteness_via_statfs(
    |                          --------------------- arguments to this function are incorrect
749 |             libc::statfs,
    |             ^^^^^^^^^^^^ expected fn pointer, found fn item
    |
    = note: expected fn pointer `unsafe extern "C" fn(*const i8, _) -> _`
                  found fn item `unsafe extern "C" fn(*const u8, _) -> _ {libc::statfs}`
note: function defined here
   --> src/path.rs:712:12
    |
712 |         fn remoteness_via_statfs<StatFS, Flags>(
    |            ^^^^^^^^^^^^^^^^^^^^^
713 |             statfn: unsafe extern "C" fn(*const i8, *mut StatFS) -> libc::c_int,
    |             -------------------------------------------------------------------

error[E0308]: mismatched types
   --> src/path.rs:725:34
    |
725 |             if unsafe { (statfn)(path.as_ptr(), buf.as_mut_ptr()) } < 0 {
    |                         -------- ^^^^^^^^^^^^^ expected `*const i8`, found `*const u8`
    |                         |
    |                         arguments to this function are incorrect
    |
    = note: expected raw pointer `*const i8`
               found raw pointer `*const u8`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `fish` (lib) due to 2 previous errors

(cherry picked from commit 91ee45b0e1)
2025-09-20 14:32:55 +02:00
Johannes Altmanninger
a27f615350 Fix crash on "bg" of non-job-controlled job
fish -c 'sleep 1 & bg %1' is supposed to fail because the job is not
under job control.

When we try to print the failure, we accidentally still
hold a borrow of the job list.  This blows up because we use
"builtin_print_help_error()" to print the failure message; that
function runs "job_reap()" which wants an exclusive borrow of the
job list. Let's drop our job list earlier.

(cherry picked from commit 26116b477e)
2025-09-20 14:03:49 +02:00
Johannes Altmanninger
46ce8a1d2f Fix regression breaking self-insert of kitty shifted codepoint
Commit 50a6e486a5 (Allow explicit shift modifier for non-ASCII
letters, fix capslock behavior, 2025-03-30) delayed handling of kitty
keyboard protocol's shifted codepoints.  It does handle shifted
codepoints when matching keys to mappings; but it fails to handle
them in the self-insert code paths where we want to insert the text
represented by CharEvent::Key.
Fix it by resolving the shifted key.

Fixes #11813

(cherry picked from commit bb916f8d73)
2025-09-20 14:03:38 +02:00
Johannes Altmanninger
03ccc88868 Move key codepoint computation to key event
For the next commit.

(cherry picked from commit 8d12dfe065)
2025-09-20 14:02:30 +02:00
Johannes Altmanninger
79cf4c3e0b build_tools/release.sh: approve macos-codesign 2025-09-19 09:53:02 +02:00
Johannes Altmanninger
701e1a3b02 github release workflow: make sure that last changelog entry isn't given spurious markup
(cherry picked from commit ff633bd744)
2025-09-18 10:46:40 +02:00
Johannes Altmanninger
b1ec703ceb Release 4.0.8
Created by ./build_tools/release.sh 4.0.8
2025-09-18 10:00:03 +02:00
Johannes Altmanninger
553612f74a github release workflow: work around trailing "---"-line in changelog
The extracted release notes trigger a sphinx warning

	/tmp/tmp.V6RGP92nc2/src/index.rst:6:Document may not end with a transition.

which we don't seem to get on the full CHANGELOG.rst.
Let's work around that for now.

(cherry picked from commit 0d46c26988)
2025-09-18 09:58:55 +02:00
Johannes Altmanninger
861decb003 build_tools/release.sh: relax assertion about changelog title
I'd like to move to a process where everything goes into master first,
and then flows downstream to any release branches (i.e. no merging
of Integration_* branches back into master).

The only thing we need to change for that is to add release notes for
patch releases eagerly on master.  That implies that we want to use
the actual version instead of ???.  (Only if something goes wrong
in the release process, we need to change this on both branches,
but that should happen too often.)

(cherry picked from commit 1840df96a2)
2025-09-18 09:38:30 +02:00
Johannes Altmanninger
3b8780aa6c build_tools/release.sh: push to the integration branch when all goes well
Also, ignore any "sendemail.to" Git config, and remove a temporary
statement in release notes.

(cherry picked from commit 3456b33050)
2025-09-18 09:32:37 +02:00
Johannes Altmanninger
7783505bee Fix new-style bindings shadowing raw escape sequence bindings
Given

	bind up "echo user up, new notation"
	bind \e\[A "echo user up, legacy notation"

prior to b9d9e7edc6 (Match bindings with explicit shift
first, 2025-05-24), we prioritized the legacy notation because
input_mapping_insert_sorted() makes us try longer sequences first --
and "up" is only one key compared to the three-key legacy sequence.

This prioritization was broken in b9d9e7edc6, causing plugins that
update to the "bind up" notation to break users who haven't (#11803).

Even worse, it caused preset bindings to shadow user ones:

	bind --preset up "echo preset up, new notation"
	bind \e\[A "echo user up, legacy notation"

Restore backwards compatibility by treating matches against legacy
notation like exact matches again.

(cherry picked from commit 9a04c15894)
2025-09-18 09:29:45 +02:00
Johannes Altmanninger
23c25ffe43 build_tools/release.sh: fixes for updating fish-site
Also check that "cd fish-site && make && make new-release" doesn't
leave behind untracked files we're not aware of.  This implies that
this script ought to refuse to run if there are untracked files,
at least in fish-site.

(cherry picked from commit 529f722d2f)
2025-09-13 11:13:17 +02:00
Johannes Altmanninger
7619fa316c Release 4.0.6
Created by ./build_tools/release.sh 4.0.6
2025-09-12 11:47:41 +02:00
Johannes Altmanninger
e2005c64b3 Backport release-script related changes from master
Will commit these to master momentarily (#10449).
2025-09-12 11:47:01 +02:00
Johannes Altmanninger
9ada3e6c16 Group changelog entries 2025-09-12 11:14:52 +02:00
Johannes Altmanninger
bdba2c227d CHANGELOG: update 2025-09-11 13:55:30 +02:00
Johannes Altmanninger
201882e72a CHANGELOG: fix inline literal RST syntax 2025-09-09 07:46:31 +02:00
Johannes Altmanninger
1db0ff9f77 Allow overriding __fish_update_cwd_osc to work around terminal bugs
See #11777

While at it, pull in the TERM=dumb check from master.

(cherry picked from commit 898cc3242b)
2025-09-05 09:39:57 +02:00
Johannes Altmanninger
9258275fe6 config_paths: fix compiled-in locale dir for installed, non-embed builds
Commit bf65b9e3a7 (Change `gettext` paths to be relocatable (#11195),
2025-03-30) broke the locale path.

Commit c3740b85be (config_paths: fix compiled-in locale dir,
2025-06-12) fixed what it calls "case 4", but "case 2" is also
affected; fix that. Before/after:

	$ ~/.local/opt/fish/bin/fish -d config
	paths.locale: /home/johannes/.local/opt/fish/share/fish/locale
	$ ~/.local/opt/fish/bin/fish -d config
	paths.locale: /home/johannes/.local/opt/fish/share/locale

See https://github.com/fish-shell/fish-shell/issues/11683#issuecomment-3218190662

(cherry picked from commit 21a07f08a3)
2025-08-29 19:29:03 +02:00
Johannes Altmanninger
6900b89c82 Add mark-prompt feature flag
So far, terminals that fail to parse OSC sequences are the only reason
for wanting to turn off OSC 133.  Let's allow to work around it by
adding a feature flag (which is implied to be temporary).

To use it, run this once, and restart fish:

    set -Ua fish_features no-mark-prompt

Tested with

    fish -i | string escape | grep 133 &&
    ! fish_features=no-mark-prompt fish -i | string escape | grep 133

See #11749
Also #11609
2025-08-27 09:17:35 +02:00
Johannes Altmanninger
9c0086b7af Backport default alt-delete binding
This is standard on macOS and in chrome/firefox.

On master, this was sneakily added in
2bb5cbc959 (Default bindings for token movements v2, 2025-03-04)
and before that in
6af96a81a8 (Default bindings for token movement commands, 2024-10-05)

Ref: https://lobste.rs/s/ndlwoh/wizard_his_shell#c_qvhnvd
2025-08-02 09:53:22 +02:00
Johannes Altmanninger
e200abe39c __fish_seen_subcommand_from: fix regression causing false negatives given multiple arguments
Fixes 2bfa7db7bc (Restructure __fish_seen_subcommand_from, 2024-07-07)
Fixes #11685

(cherry picked from commit 4412164fd4)
2025-07-26 20:36:53 +02:00
Johannes Altmanninger
0c8f1f4220 Fix async-signal safety in SIGTERM handler
Cherry-picked from
- 941701da3d (Restore some async-signal discipline to SIGTERM, 2025-06-15)
- 81d45caa76e (Restore terminal state on SIGTERM again, 2025-06-21)

Also, be more careful in terminal_protocols_disable_ifn about accessing
reader_current_data(), as pointed out in 65a4cb5245 (Revert "Restore terminal
state on SIGTERM again", 2025-07-19).

See #11597
2025-07-26 13:09:18 +02:00
Johannes Altmanninger
e593da1c2e Increase timeout when reading escape sequences inside paste/kitty kbd
Historically, fish has treated input bytes [0x1b, 'b'] as alt-b (rather than
"escape,b") if the second byte arrives within 30ms of the first.

Since we made builtin bind match key events instead of raw byte sequences,
we have another place where we do similar disambiguation: when we read keys
such as alt-left ("\e[1;3D"), we only consider bytes to be part of this
sequence if stdin is immediately readable (actually "readable after a 1ms
timeout" since e1be842 (Work around torn byte sequences in qemu kbd input
with 1ms timeout, 2025-03-04)).

This is technically wrong but has worked in practice (for Kakoune etc.).

Issue #11668 reports two issues on some Windows terminals feeding a remote
fish shell:
- the "bracketed paste finished" sequence may be split into multiple packets,
  which causes a delay of > 1ms between individual bytes being readable.
- AutoHotKey scripts simulating seven "left" keys result in sequence tearing
  as well.

Try to fix the paste case by increasing the timeout when parsing escape
sequences.

Also increase the timeout for terminals that support the kitty keyboard
protocol.  The user should only notice this new delay after pressing one of
escape,O, escape,P, escape,[, or escape,] **while the kitty keyboard protocol
is disabled** (e.g. while an external command is running).  In this case,
the fish_escape_delay_ms is also virtually increased; hopefully this edge
case is not ever relevant.

Part of #11668

(cherry picked from commit 30ff3710a0)
2025-07-25 18:29:19 +02:00
Johannes Altmanninger
6666c8f1cd Block interrupts and uvar events while decoding key
readb() has only one caller that passes blocking=false: try_readb().
This function is used while decoding keys; anything but a successful read
is treated as "end of input sequence".

This means that key input sequences such as \e[1;3D
can be torn apart by
- signals (EINTR) which is more likely since e1be842 (Work around torn byte
  sequences in qemu kbd input with 1ms timeout, 2025-03-04).
- universal variable notifications (from other fish processes)

Fix this by blocking signals and not listening on the uvar fd.  We do something
similar when matching key sequences against bindings, so extract a function
and use it for key decoding too.

Ref: https://github.com/fish-shell/fish-shell/issues/11668#issuecomment-3101341081
(cherry picked from commit da96172739)
2025-07-25 18:21:27 +02:00
Johannes Altmanninger
3e61036911 Revert "Change readch() into try_readch()"
try_readch() was added to help a fuzzing harness, specifically to avoid a
call to `unreachable!()` in the NothingToRead case.  I don't know much about
that but it seems like we should find a better way to tell the fuzzer that
this can't happen.

Fortunately the next commit will get rid of readb()'s "blocking" argument,
along the NothingToRead enum variant. So we'll no longer need this.

This reverts commit b92830cb17.

(cherry picked from commit fb7ee0db74)
2025-07-25 18:21:27 +02:00
Johannes Altmanninger
f23a479b81 Reduce MaybeUninit lifetime
(cherry picked from commit 137f220225)
2025-07-25 18:21:27 +02:00
王宇逸
e274ff41d0 Use uninit instead of zeroed (src/input_common.rs)
(cherry picked from commit 7c2c7f5874)
2025-07-25 18:21:27 +02:00
Johannes Altmanninger
d2af306f3d Fix some unused-ControlFlow warnings 2025-07-25 18:11:04 +02:00
Johannes Altmanninger
0e7c7f1745 Remove unused import
(cherry picked from commit 07ff4e7df0)
2025-07-25 11:57:21 +02:00
Johannes Altmanninger
3fada80553 Fix regression causing \e[ to be interpreted as ctrl-[
Fixes 3201cb9f01 (Stop parsing invalid CSI/SS3 sequences as alt-[/alt-o,
2024-12-30).

(cherry picked from commit 43d583d991)
2025-07-25 11:49:07 +02:00
Johannes Altmanninger
c7d4acbef8 Update changelog 2025-06-29 16:01:44 +02:00
Johannes Altmanninger
e204a4c126 Add ctrl-alt-h compatibility binding
Historically, ctrl-i sends the same code as tab, ctrl-h sends backspace and
ctrl-j and ctrl-m behave like enter.

Even for terminals that send unambiguous encodings (via the kitty keyboard
protocol), we have kept bindings like ctrl-h, to support existing habits.

We forgot that pressing alt-ctrl-h would behave like alt-backspace (and can
be easier to reach) so maybe we should add that as well.

Don't add ctrl-shift-i because at least on Linux, that's usually intercepted
by the terminal emulator.

Technically there are some more such as "ctrl-2" (which used to do the same as
"ctrl-space") but I don't think anyone uses that over "ctrl-space".

Closes #https://github.com/fish-shell/fish-shell/discussions/11548

(cherry picked from commit 4d67ca7c58)
2025-06-28 14:20:03 +02:00
Johannes Altmanninger
9af33802ec Use statvfs on NetBSD again to fix build
From commit ba00d721f4 (Correct statvfs call to statfs, 2025-06-19):

> This was missed in the Rust port

To elaborate:

- ec176dc07e (Port path.h, 2023-04-09) didn't change this (as before,
 `statvfs` used `ST_LOCAL` and `statfs` used `MNT_LOCAL`)
- 6877773fdd (Fix build on NetBSD (#10270), 2024-01-28) changed the `statvfs`
  call to `statfs`, presumably due to the libc-wrapper for
  `statvfs` being missing on NetBSD.  This change happens
  to work fine on NetBSD because they do [`#define ST_LOCAL
  MNT_LOCAL`](https://github.com/fish-shell/fish-shell/pull/11486#discussion_r2092408952)
  But it was wrong on others like macOS and FreeBSD, which was fixed by
  ba00d721f4 (but that broke the build on NetBSD).
- 7228cb15bf (Include sys/statvfs.h for the definition of ST_LOCAL (Rust
  port regression), 2025-05-16)
  fixed a code clone left behind by the above commit (incorrectly assuming
  that the clone had always existed.)

Fix the NetBSD build specifically by using statfs on that platform.

Note that this still doesn't make the behavior equivalent to commit LastC++11.
That one used ST_LOCAL if defined, and otherwise MNT_LOCAL if defined.

If we want perfect equivalence, we could detect both flags in `src/build.rs`.
Then we would also build on operating systems that define neither. Not sure.

Closes #11596

(cherry picked from commit 6644cc9b0e)
2025-06-28 11:46:25 +02:00
王宇逸
eecf0814a1 Use uninit instead of zeroed (cherry-pikcked only the change to src/path.rs)
(cherry picked from commit 7c2c7f5874)
2025-06-28 11:46:25 +02:00
Johannes Altmanninger
1ceebdf580 Fix some CSI commands being sent to old midnight commander
Commit 97581ed20f (Do send bracketed paste inside midnight commander,
2024-10-12) accidentally started sending CSI commands such as "CSI >5;0m",
which we intentionally didn't do for some old versions of Midnight Commander,
which fail to parse them. Fix that.

Fixes #11617
2025-06-25 13:36:03 +02:00
Johannes Altmanninger
335f91babd completions/git: fix spurious error when no subcommand is in $PATH
Systems like NixOS might not have "git-receive-pack" or any other "git-*"
executable in in $PATH -- instead they patch git to use absolute paths.
This is weird. But no reason for us to fail. Silence the error.

Fixes #11590

(cherry picked from commit 4f46d369c4)
2025-06-24 11:59:57 +08:00
Johannes Altmanninger
ec66749369 __fish_complete_list: only unescape "$(commandline -t)"
Commit cd3da62d24 (fix(completion): unescape strings for __fish_complete_list,
2024-09-17) bravely addressed an issue that exists in a lot of completions.
It did so only for __fish_complete_list. Fair enough.

Unfortunately it unescaped more than just "$(commandline -t)".

This causes the problem described at
https://github.com/fish-shell/fish-shell/issues/11508#issuecomment-2889088934
where completion descriptions containing a backslash followed by "n" are
interpreted as newlines, breaking the completion parser.  Fix that.

(cherry picked from commit 60881f1195)
2025-06-21 18:58:33 +02:00
Johannes Altmanninger
6e9e33d81d __fish_complete_list: strip "--foo=" prefix from replacing completions
Given a command line like

	foo --foo=bar=baz=qux\=argument

(the behavior is the same if '=' is substituted with ':').

fish completes arguments starting from the last unescaped separator, i.e.

	foo --foo=bar=baz=qux\=argument
			 ^
__fish_complete_list provides completions like

	printf %s\n (commandline -t)(printf %s\n choice1 choice2 ...)

This means that completions include the "--foo=bar=baz=" prefix.

This is wrong. This wasn't a problem until commit f9febba (Fix replacing
completions with a -foo prefix, 2024-12-14), because prior to that, replacing
completions would replace the entire token.
This made it too hard to writ ecompletions like

	complete -c foo -s s -l long -xa "hello-world goodbye-friend"

that would work with "foo --long fri" as well as "foo --long=frie".
Replacing the entire token would only work if the completion included that
prefix, but the above command is supposed to just work.
So f9febba made us replace only the part after the separator.

Unfortunately that caused the earlier problem.  Work around this.  The change
is not pretty, but it's a compromise until we have a better way of telling
which character fish considers to be the separator.

Fixes #11508

(cherry picked from commit 320ebb6859)
2025-06-21 18:58:17 +02:00
Peter Ammon
f3ebc68d5d Correct statvfs call to statfs
This was missed in the Rust port - C++ had statfs for MNT_LOCAL and not statvfs.
The effect of this is that fish never thought its filesystem was local on macOS
or BSDs (Linux was OK). This caused history race tests to fail, and also could
in rare cases result in history items being dropped with multiple concurrent
sessions.

This fixes the history race tests under macOS and FreeBSD - we weren't locking
because we thought the history was a remote file.

Cherry-picked from ba00d721f4
2025-06-19 15:36:19 -07:00
Johannes Altmanninger
4be17bfefb fixup! Extract config path module. NFC
Fix cargo (non-cmake) build.
2025-06-13 12:17:10 +02:00
Johannes Altmanninger
6fd0025f38 Make LOCALEDIR relocatable as well
As explained in c3740b85be (config_paths: fix compiled-in locale dir,
2025-06-12), fish is "relocatable", i.e. "mv /usr/ /usr2/" will leave
"/usr2/bin/fish" fully functional.

There is one exception: for LOCALEDIR we always use the path determined at
compile time.
This seems wrong; let's use the same relocatable-logic as for other paths.

Inspired by bf65b9e3a7 (Change `gettext` paths to be relocatable (#11195),
2025-03-30).
2025-06-13 11:52:46 +02:00
Johannes Altmanninger
052fc18db9 Extract config path module. NFC
This is the "code movement" part of bf65b9e3 ("Change `gettext` paths to be
relocatable (#11195)").

While at it, fix some warnings.
2025-06-13 11:52:46 +02:00
Integral
63a08e53e5 Replace some PathBuf with Path avoid unnecessary heap allocation (#10929)
(cherry picked from commit b19a467ea6)
2025-06-11 11:38:53 +02:00
Erick Howard
62ac23453e Code cleanup in src/bin/fish.rs to make it more idiomatic (#10975)
Code cleanup in `src/bin/fish.rs` to make it more idiomatic

(cherry picked from commit 967c4b2272)
2025-06-11 11:31:05 +02:00
Johannes Altmanninger
c052beb4dd Fix build on Rust 1.70 2025-06-10 17:56:21 +02:00
Johannes Altmanninger
e0cabacdaa kitty keyboard protocol: fall back to base layout key
On terminals that do not implement the kitty keyboard protocol "ctrl-ц" on
a Russian keyboard layout generally sends the same byte as "ctrl-w". This
is because historically there was no standard way to encode "ctrl-ц",
and the "ц" letter happens to be in the same position as "w" on the PC-101
keyboard layout.

Users have gotten used to this, probably because many of them are switching
between a Russian (or Greek etc.) and an English layout.

Vim/Emacs allow opting in to this behavior by setting the "input method"
(which probably means "keyboard layout").

Match key events that have the base layout key set against bindings for
that key.

Closes #11520

---

Alternatively, we could add the relevant preset bindings (for "ctrl-ц" etc.)
but
1. this will be wrong if there is a disagreement on the placement of "ц" between two layouts
2. there are a lot of them
3. it won't work for user bindings (for better or worse)

(cherry picked from commit 7a79728df3)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
59b9f57802 fish_key_reader: unopinionated description for bind notation variants
As explained in the parent commit, "alt-+" is usually preferred over
"alt-shift-=" but both have their moments. We communicate this via a comment
saying "# recommended notation". This is not always true and not super helpful,
especially as we add a third variant for #11520 (physical key), which is
the recommended one for users who switch between English and Cyrillic layouts.

Only explain what each variant does. Based on this the user may figure out
which one to use.

(cherry picked from commit 4cbd1b83f1)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
65fc2b539c Fix some invalid assertions parsing keys
For example the terminal sending « CSI 55296 ; 5 u » would crash fish.

(cherry picked from commit c7391d1026)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
3f1add9e21 Sanitize some inputs in CSI parser
This was copied from C++ code but we have overflow checks, which
forces us to actually handle errors.

While at it, add some basic error logging.

Fixes #11092

(cherry picked from commit 4c28a7771e)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
68d2cafa6e input: remove unnecessary check in bracketed paste code path
When "self.paste_is_buffering()" is true, "parse_escape_sequence()" explicitly
returns "None" instead of "Some(Escape)".  This is irrelevant because this
return value is never read, as long as "self.paste_is_buffering()" remains
true until "parse_escape_sequence()" returns, because the caller will return
early in that case. Paste buffering only ends if we actually read a complete
escape sequence (for ending bracketed paste).

Remove this extra branch.

(cherry picked from commit e5fdd77b09)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
b9d9e7edc6 Match bindings with explicit shift first
The new key notation canonicalizes aggressively, e.g.  these two bindings
clash:

	bind ctrl-shift-a something
	bind shift-ctrl-a something else

This means that key events generally match at most one active binding that
uses the new syntax.

The exception -- two coexisting new-syntax binds that match the same key
event -- was added by commit 50a6e486a5 (Allow explicit shift modifier for
non-ASCII letters, fix capslock behavior, 2025-03-30):

	bind ctrl-A 'echo A'
	bind ctrl-shift-a 'echo shift-a'

The precedence was determined by definition order.
This doesn't seem very useful.

A following patch wants to resolve #11520 by matching "ctrl-ц" events against
"ctrl-w" bindings. It would be surprising if a "ctrl-w" binding shadowed a
"ctrl-ц" one based on something as subtle as definition order.  Additionally,
definition order semantics (which is an unintended cause of the implementation)
is not really obvious.  Reverse definition order would make more sense.

Remove the ambiguity by always giving precedence to bindings that use
explicit shift.

Unrelated to this, as established in 50a6e486a5, explicit shift is still
recommended for bicameral letters but not typically for others -- e.g. alt-+
is typically preferred over alt-shift-= because the former also works on a
German keyboard.

See #11520

(cherry picked from commit 08c8afcb12)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
6b17ec7dae Allow explicit shift modifier for non-ASCII letters, fix capslock behavior
We canonicalize "ctrl-shift-i" to "ctrl-I".
Both when deciphering this notation (as given to builtin bind),
and when receiving it as a key event ("\e[105;73;6u")

This has problems:

A. Our bind notation canonicalization only works for 26 English letters.
   For example, "ctrl-shift-ä" is not supported -- only "ctrl-Ä" is.
   We could try to fix that but this depends on the keyboard layout.
   For example "bind alt-shift-=" and "bind alt-+" are equivalent on a "us"
   layout but not on a "de" layout.
B. While capslock is on, the key event won't include a shifted key ("73" here).
   This is due a quirk in the kitty keyboard protocol[^1].  This means that
   fish_key_reader's canonicalization doesn't work (unless we call toupper()
   ourselves).

I think we want to support both notations.

It's recommended to match all of these (in this order) when pressing
"ctrl-shift-i".

	1. bind ctrl-shift-i do-something
	2. bind ctrl-shift-I do-something
	3. bind ctrl-I do-something
	4. bind ctrl-i do-something

Support 1 and 3 for now, allowing both bindings to coexist. No priorities
for now. This solves problem A, and -- if we take care to use the explicit
shift notation -- problem B.

For keys that are not affected by capslock, problem B does not apply.  In this
case, recommend the shifted notation ("alt-+" instead of "alt-shift-=")
since that seems more intuitive.
Though if we prioritized "alt-shift-=" over "alt-+" as per the recommendation,
that's an argument against the shifted key.

Example output for some key events:

	$ fish_key_reader -cV
	# decoded from: \e\[61:43\;4u
	bind alt-+ 'do something' # recommended notation
	bind alt-shift-= 'do something'
	# decoded from: \e\[61:43\;68u
	bind alt-+ 'do something' # recommended notation
	bind alt-shift-= 'do something'

	# decoded from: \e\[105:73\;6u
	bind ctrl-I 'do something'
	bind ctrl-shift-i 'do something' # recommended notation
	# decoded from: \e\[105\;70u
	bind ctrl-shift-i 'do something'

Due to the capslock quirk, the last one has only one matching representation
since there is no shifted key.  We could decide to match ctrl-shift-i events
(that don't have a shifted key) to ctrl-I bindings (for ASCII letters), as
before this patch. But that case is very rare, it should only happen when
capslock is on, so it's probably not even a breaking change.

The other way round is supported -- we do match ctrl-I events (typically
with shifted key) to ctrl-shift-i bindings (but only for ASCII letters).
This is mainly for backwards compatibility.

Also note that, bindings without other modifiers currently need to use the
shifted key (like "Ä", not "shift-ä"), since we still get a legacy encoding,
until we request "Report all keys as escape codes".

[^1]: <https://github.com/kovidgoyal/kitty/issues/8493>

(cherry picked from commit 50a6e486a5)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
08d796890a Stop accepting "bind shift-A"
This notation doesn't make sense, use either A or shift-a.  We accept it
for ASCII letters only -- things like "bind shift-!" or "bind shift-Ä"
do not work as of today, we don't tolerate extra shift modifiers yet.
So let's remove it for consistency.

Note that the next commit will allow the shift-A notation again, but it will
not match shift-a events.

(cherry picked from commit 7f25d865a9)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
4f98ef36f6 Extract KeyEvent type
The be used in the grandchild commit.

(cherry picked from commit 855a1f702e)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
a09c78491f Extract function for creating key event with modifiers
(cherry picked from commit fabbbba037)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
02932d6b8c Extract constant for the number of function keys
Switch to fish_wcstoul because we want the constant to be unsigned.
It's u32 because most callers of function_key() want that.

(cherry picked from commit e9d1cdfe87)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
7cca98bda2 Fix regression decoding function keys
Commit 109ef88831 (Add menu and printscreen keys, 2025-01-01)
accidentally broke an assumption by inverting f1..f12.  Fix that.

Fixes #11098

(cherry picked from commit d2b2c5286a)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
b77fc28692 Add menu and printscreen keys
These aren't typically used in the terminal but they are present on
many keyboards.

Also reorganize the named key constants a bit.  Between F500 and
ENCODE_DIRECT_BASE (F600) we have space for 256 named keys.

(cherry picked from commit 109ef88831)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
3475531ef7 Also ignore invalid recursive escape sequences
We parse "\e\e[x" as alt-modified "Invalid" key.  Due to this extra
modifier, we accidentally add it to the input queue, instead of
dropping this invalid key.

We don't really want to try to extract some valid keys from this
invalid sequence, see also the parent commit.

This allows us to remove misplaced validation that was added by
e8e91c97a6 (fish_key_reader: ignore sentinel key, 2024-04-02) but
later obsoleted by 66c6e89f98 (Don't add collateral sentinel key to
input queue, 2024-04-03).

(cherry picked from commit 84f19a931d)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
8222ed891b Stop parsing invalid CSI/SS3 sequences as alt-[/alt-o
This situation can be triggered in practice inside a terminal like tmux
3.5 by running

	tmux new-session fish -C 'sleep 2' -d reader -o log-file

and typing "alt-escape x"

The log shows that we drop treat this as alt-[ and drop  the x on the floor.

	reader: Read char alt-\[ -- Key { modifiers: Modifiers { ctrl: false,
	alt: true, shift: false }, codepoint: '[' } -- [27, 91, 120]

This input ("\e[x") is ambiguous.

It looks like it could mean "alt-[,x".  However that conflicts with a
potential future CSI code, so it makes no sense to try to support this.

Returning "None" from parse_csi() causes this weird behavior of
returning "alt-[" and dropping the rest of the parsed sequence.
This is too easy; it has even crept into a bunch of places
where the input sequence is actually valid like "VT200 button released"
but where we definitely don't want to report any key.

Fix the default: report no key for all unknown sequences and
intentionally-suppressed sequences.  Treat it at "alt-[" only when
there is no input byte available, which is more or less unambiguous,
hence a strong enough signal that this is a actually "alt-[".

(cherry picked from commit 3201cb9f01)
2025-06-10 16:50:00 +02:00
Johannes Altmanninger
f787e6858c Fix tests/checks/autoload.fish
Apparently this test runs with "build/tests" as CWD when run with cmake.
2025-06-10 16:50:00 +02:00
Fabian Boehm
a014166795 completions/nmcli: Complete at runtime
This used to get all the interfaces and ssids when the completions
were loaded. That's obviously wrong, given that ssids especially can, you know, change

(cherry picked from commit 9116c61736)

cherry-picking since this easy to trigger
(seen again in https://github.com/fish-shell/fish-shell/pull/11549)
2025-06-06 11:52:17 +02:00
Johannes Altmanninger
f7e639504a completions/git: improve idempotency in case of double load
As mentioned in the previous few commits and in #11535, running
"set fish_complete_path ..."  and "complete -C 'git ...'"  may result in
"share/completions/git.fish" being loaded multiple times.

This is usually fine because fish internally erases all cached completions
whenever fish_complete_path changes.

Unfortunately there is at least global variable that grows each time git.fish
is sourced. This doesn't make a functional difference but it does slow
down completions.  Fix that by resetting the variable at load time.

(cherry picked from commit 4b5650ee4f)
2025-05-29 18:01:22 +02:00
Johannes Altmanninger
028b60cad6 Fix "set fish_complete_path" accidentally disabling autoloading
Commit 5918bca1eb (Make "complete -e" prevent completion autoloading,
2024-08-24) makes "complete -e foo" add a tombstone for "foo", meaning we
will never again load completions for "foo".

Due to an oversight, the same tombstone is added when we clear cached
completions after changing "fish_complete_path", preventing completions from
being loaded in that case.  Fix this by restoring the old behavior unless
the user actually used "complete -e".

(cherry picked from commit a7c04890c9)
2025-05-29 18:01:04 +02:00
Johannes Altmanninger
b11e22d905 Fix uvar file mtime force-update (Rust port regression)
When two fish processes rewrite the uvar file concurrent, they rely on the
uvar file's mtime (queried after taking a lock, if locking is supported) to
tell us whether their view of the uvar file is still up-to-date.  If it is,
they proceed to move it into place atomically via rename().

Since the observable mtime only updates on every OS clock tick, we call
futimens() manually to force-update that, to make sure that -- unless both
fish conincide on the same *nanosecond* -- other fish will notice that the
file changed.

Unfortunately, commit 77aeb6a2a8 (Port execution, 2023-10-08) accidentally
made us call futimens() only if clock_gettime() failed, instead of when
it succeeded. This means that we need to wait for the next clock tick to
observe a change in mtime.
Any resulting false negatives might have caused us to drop universal variable updates.

Reported in https://github.com/fish-shell/fish-shell/pull/11492#discussion_r2098948362

See #10300

(cherry picked from commit 8617964d4d)
2025-05-23 08:50:17 +02:00
Johannes Altmanninger
33f8415785 Fixup history file EINTR loop to actually loop
Fixes d84e68dd4f (Retry history file flock() on EINTR, 2025-05-20).

(cherry picked from commit 3867163193)
2025-05-20 17:19:12 +02:00
Johannes Altmanninger
5ccd155177 Retry history file flock() on EINTR
When locking the uvar file, we retry whenever flock() fails with EINTR
(e.g. due to ctrl-c).

But not when locking the history file.  This seems wrong; all other libc
functions in the "history_file" code path do retry.

Fix that. In future we should extract a function.

Note that there are other inconsistencies; flock_uvar_file() does not
shy away from remote file systems and does not respect ABANDONED_LOCKING.
This means that empirically probably neither are necessary; let's make things
consistent in future.

See https://github.com/fish-shell/fish-shell/pull/11492#discussion_r2095096200
Might help #10300

(cherry picked from commit 4d84e68dd4)
2025-05-20 15:32:56 +02:00
Johannes Altmanninger
0f8d3a5174 Revert "Temporarily enable history_file debug category by default"
Commit f906a949cf (Temporarily enable history_file debug category by default,
2024-10-09) enabled the "history_file" debug category by default to gather
more data.

Judging from
https://github.com/fish-shell/fish-shell/issues/10300#issuecomment-2876718382
the logs didn't help, or were at least not visible when logging to stderr
(due to reboot).

Let's disable "history_file" logs again to remove potential
noise if the file system is read-only, disk is full etc., see
https://github.com/fish-shell/fish-shell/pull/11492#discussion_r2094781120

See #10300

(cherry picked from commit 285a810814)
2025-05-20 15:31:57 +02:00
Johannes Altmanninger
c4a26cb2b1 builtin status: remove spurious newline from current-command (Rust port regression)
WHen "status current-command" is called outside a function it always returns
"fish". An extra newline crept in, fix that.

Fixes 77aeb6a2a8 (Port execution, 2023-10-08).
Fixes #11503

(cherry picked from commit e26b585ce5)
2025-05-20 12:33:01 +02:00
Johannes Altmanninger
7228cb15bf Include sys/statvfs.h for the definition of ST_LOCAL (Rust port regression)
See https://man.netbsd.org/statvfs.5.
According to https://github.com/NetBSD/src/blob/trunk/sys/sys/statvfs.h#L135,
NetBSD has "#define ST_LOCAL MNT_LOCAL".  So this commit likely makes no
difference on existing systems.

While at it
- comment include statements
- remove a code clone

See #11486

(cherry picked from commit d68f8bdd3b)
2025-05-16 08:21:56 +02:00
Alan Somers
d5b46d6535 Fix remote filesystem detection on FreeBSD
Need an extra include to get the definition of MNT_LOCAL

Fixes #11483

(cherry picked from commit 7f4998ad9b)
2025-05-16 08:21:25 +02:00
Johannes Altmanninger
d4b4d44f14 Fix Vi mode glitch when replacing at last character
Another regression from d51f669647 (Vi mode: avoid placing cursor beyond last
character, 2024-02-14) "Unfortunately Vi mode sometimes needs to temporarily
select past end". So do the replace_one mode bindings which were forgotten.

Fix this.

This surfaces a tricky problem: when we use something like

	bind '' self-insert some-command

When key event "x" matches this generic binding, we insert both "self-insert"
and "some-command" at the front of the queue, and do *not* consume "x",
since the binding is empty.

Since there is a command (that might call "exit"), we insert a check-exit
event too, after "self-insert some-command" but _before_ "x".

The check-exit event makes "self-insert" do nothing. I don't think there's a
good reason for this; self-insert can only be triggered by a key event that
maps to self-insert; so there must always be a real key available for it to
consume. A "commandline -f self-insert" is a nop. Skip check-exit here.

Fixes #11484

(cherry picked from commit 107e4d11de)
2025-05-13 00:31:22 +02:00
Johannes Altmanninger
b8cfd6d12b Fix typo causing wrong cursor position after Vi mode paste
Regressed in d51f669647 (Vi mode: avoid placing cursor beyond last character,
2024-02-14).

(cherry picked from commit 50500ec5b9)
2025-05-13 00:31:09 +02:00
Johannes Altmanninger
6fb22a4fd1 Fix regression causing crash indenting commandline with "$()"
Commit b00899179f (Don't indent multi-line quoted strings; do indent inside
(), 2024-04-28) changed how we compute indents for string tokens with command
substitutions:

	echo "begin
	not indented
	end $(
	begin
	    indented
	end)"(
	begin
	    indented
	end
	)

For the leading quoted part of the string, we compute indentation only for
the first character (the opening quote), see 4c43819d32 (Fix crash indenting
quoted suffix after command substitution, 2024-09-28).

The command substitutions, we do indent as usual.

To implement the above, we need to separate quoted from non-quoted
parts. This logic crashes when indent_string_part() is wrongly passed
is_double_quoted=true.

This is because, given the string "$()"$(), parse_util_locate_cmdsub calls
quote_end() at index 4 (the second quote). This is wrong because that function
should only be called at opening quotes; this is a closing quote. The opening
quote is virtual here. Hack around this.

Fixes #11444

(cherry picked from commit 48704dc612)
2025-05-12 21:41:10 +02:00
Johannes Altmanninger
35849c57dc Explicit type for "$()" hack in parse_util_locate_cmdsub
(cherry picked from commit 8abab0e2cc)
2025-05-12 21:41:10 +02:00
Johannes Altmanninger
27504658ce Remove code clone in parse_util_locate_cmdsub
(cherry picked from commit bd178c8ba8)
2025-05-12 21:41:10 +02:00
Johannes Altmanninger
db323348c7 Set transient command line in custom completions (Rust port regression)
Commit df3b0bd89f (Fix commandline state for custom completions with variable
overrides, 2022-01-26) made us push a transient command line for custom
completions based on a tautological null-pointer check ("var_assignments").

Commit 77aeb6a2a8 (Port execution, 2023-10-08) turned the null pointer into
a reference and replaced the check with "!ad.var_assignments.is_empty()".
This broke scenarios that relied on the transient commandline.  In particular
the attached test cases rely on the transient commandline implicitly placing
the cursor at the end, irrespective of the cursor in the actual commandline.

I'm not sure if there is an easy way to identify these scenarios.

Let's restore historical behavior by always pushing the transient command line.

Fixes #11423

(cherry picked from commit 97641c7bf6)
2025-05-12 21:39:42 +02:00
Johannes Altmanninger
edb1b5f333 Share alt-{b,f} with Vi mode, to work around Terminal.app/Ghostty more
Commit f4503af037 (Make alt-{b,f} move in directory history if commandline is
empty, 2025-01-06) had the intentional side effect of making alt-{left,right}
(move in directory history) work in Terminal.app and Ghostty without other,
less reliable workarounds.
That commit says "that [workaround] alone should not be the reason for
this change."; maybe this was wrong.

Extend the workaround to Vi mode.  The intention here is to provide
alt-{left,right} in Vi mode.  This also adds alt-{b,f} which is odd but
mostly harmless (?) because those don't do anything else in Vi mode.
It might be confusing when studying "bind" output but that one already has
almost 400 lines for Vi mode.

Closes #11479

(cherry picked from commit 3081d0157b)
2025-05-12 21:35:47 +02:00
Daniel Rainer
2d8d377ddc Make printf unicode-aware
Specifically, the width and precision format specifiers are interpreted as
referring to the width of the grapheme clusters rather than the byte count of
the string. Note that grapheme clusters can differ in width.

If a precision is specified for a string, meaning its "maximum number of
characters", we consider this to limit the width displayed.
If there is a grapheme cluster whose width is greater than 1,
it might not be possible to get precisely the desired width.
In such cases, this last grapheme cluster is excluded from the output.

Note that the definitions used here are not consistent with the `string length`
builtin at the moment, but this has already been the case.

(cherry picked from commit 09eae92888)
2025-05-12 21:33:40 +02:00
Peter Ammon
057dd930b4 Changelog fix for #11354 2025-05-08 18:42:57 -07:00
Cuichen Li
25b944e3e6 Revert "Work around $PATH issues under WSL (#10506)"
This reverts commit 3374692b91.
2025-05-08 18:41:02 -07:00
93 changed files with 2280 additions and 925 deletions

View File

@@ -13,16 +13,20 @@ max_line_length = 100
indent_style = tab
[*.{md,rst}]
max_line_length = unset
trim_trailing_whitespace = false
[*.{sh,ac}]
indent_size = 2
[*.sh]
indent_size = 4
[build_tools/release.sh]
max_line_length = 72
[Dockerfile]
indent_size = 2
[share/{completions,functions}/**.fish]
max_line_length = off
max_line_length = unset
[{COMMIT_EDITMSG,git-revise-todo}]
max_line_length = 80
[{COMMIT_EDITMSG,git-revise-todo,*.jjdescription}]
max_line_length = 72

View File

@@ -0,0 +1,14 @@
name: Install sphinx-markdown-builder
permissions:
contents: read
runs:
using: "composite"
steps:
- shell: bash
run: |
set -x
commit=b259de1dc97573a71470a1d71c3d83535934136b
pip install git+https://github.com/krobelus/sphinx-markdown-builder@"$commit"
python -c 'import sphinx_markdown_builder'

View File

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

View File

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

View File

@@ -1,42 +0,0 @@
name: macOS build and codesign
on:
workflow_dispatch: # Enables manual trigger from GitHub UI
jobs:
build-and-code-sign:
runs-on: macos-latest
environment: macos-codesign
steps:
- uses: actions/checkout@v4
- name: Install Rust 1.73.0
uses: dtolnay/rust-toolchain@1.73.0
with:
targets: x86_64-apple-darwin
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: build-and-codesign
run: |
cargo install apple-codesign
mkdir -p "$FISH_ARTEFACT_PATH"
echo "$MAC_CODESIGN_APP_P12_BASE64" | base64 --decode > /tmp/app.p12
echo "$MAC_CODESIGN_INSTALLER_P12_BASE64" | base64 --decode > /tmp/installer.p12
echo "$MACOS_NOTARIZE_JSON" > /tmp/notarize.json
./build_tools/make_pkg.sh -s -f /tmp/app.p12 -i /tmp/installer.p12 -p "$MAC_CODESIGN_PASSWORD" -n -j /tmp/notarize.json
rm /tmp/installer.p12 /tmp/app.p12 /tmp/notarize.json
env:
MAC_CODESIGN_APP_P12_BASE64: ${{ secrets.MAC_CODESIGN_APP_P12_BASE64 }}
MAC_CODESIGN_INSTALLER_P12_BASE64: ${{ secrets.MAC_CODESIGN_INSTALLER_P12_BASE64 }}
MAC_CODESIGN_PASSWORD: ${{ secrets.MAC_CODESIGN_PASSWORD }}
MACOS_NOTARIZE_JSON: ${{ secrets.MACOS_NOTARIZE_JSON }}
# macOS runners keep having issues loading Cargo.toml dependencies from git (GitHub) instead
# of crates.io, so give this a try. It's also sometimes significantly faster on all platforms.
CARGO_NET_GIT_FETCH_WITH_CLI: true
FISH_ARTEFACT_PATH: /tmp/fish-built
- uses: actions/upload-artifact@v4
with:
name: macOS Artefacts
path: /tmp/fish-built/*
if-no-files-found: error

View File

@@ -22,6 +22,7 @@ jobs:
sudo apt install gettext libpcre2-dev python3-pexpect tmux
# Generate a locale that uses a comma as decimal separator.
sudo locale-gen fr_FR.UTF-8
- uses: ./.github/actions/install-sphinx-markdown-builder
- name: cmake
run: |
mkdir build && cd build

190
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,190 @@
name: Create a new release
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (tag name)'
required: true
type: string
permissions:
contents: write
jobs:
is-release-tag:
name: Pre-release checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Workaround for https://github.com/actions/checkout/issues/882
ref: ${{ inputs.version }}
- name: Check if the pushed tag looks like a release
run: |
set -x
commit_subject=$(git log -1 --format=%s)
tag=$(git describe)
[ "$commit_subject" = "Release $tag" ]
source-tarball:
needs: [is-release-tag]
name: Create the source tarball
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
tarball-name: ${{ steps.version.outputs.tarball-name }}
steps:
- uses: actions/checkout@v4
with:
# Workaround for https://github.com/actions/checkout/issues/882
ref: ${{ inputs.version }}
- name: Install dependencies
run: sudo apt install cmake gettext ninja-build python3-pip python3-sphinx
- uses: ./.github/actions/install-sphinx-markdown-builder
- name: Create tarball
run: |
set -x
mkdir /tmp/fish-built
FISH_ARTEFACT_PATH=/tmp/fish-built ./build_tools/make_tarball.sh
relnotes=/tmp/fish-built/release-notes.md
# Need history since the last release (i.e. tag) for stats.
git fetch --tags
git fetch --unshallow
sh -x ./build_tools/release-notes.sh >"$relnotes"
# Delete title
sed -n 1p "$relnotes" | grep -q "^## fish .*"
sed -n 2p "$relnotes" | grep -q '^$'
sed -i 1,2d "$relnotes"
- name: Upload tarball artifact
uses: actions/upload-artifact@v4
with:
name: source-tarball
path: |
/tmp/fish-built/fish-${{ inputs.version }}.tar.xz
/tmp/fish-built/release-notes.md
if-no-files-found: error
packages-for-linux:
needs: [is-release-tag]
name: Build single-file fish for Linux (experimental)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Workaround for https://github.com/actions/checkout/issues/882
ref: ${{ inputs.version }}
- name: Install Rust Stable
uses: ./.github/actions/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl
- name: Install dependencies
run: sudo apt install crossbuild-essential-arm64 musl-tools python3-sphinx
- name: Build statically-linked executables
run: |
set -x
CFLAGS="-D_FORTIFY_SOURCE=2" \
CMAKE_WITH_GETTEXT=0 \
CC=aarch64-linux-gnu-gcc \
RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc -C link-arg=-lgcc -C link-arg=-D_FORTIFY_SOURCE=0" \
cargo build --release --target aarch64-unknown-linux-musl --bin fish
cargo build --release --target x86_64-unknown-linux-musl --bin fish
- name: Compress
run: |
set -x
for arch in x86_64 aarch64; do
tar -cazf fish-$(git describe)-linux-$arch.tar.xz \
-C target/$arch-unknown-linux-musl/release fish
done
- uses: actions/upload-artifact@v4
with:
name: Static builds for Linux
path: fish-${{ inputs.version }}-linux-*.tar.xz
if-no-files-found: error
create-draft-release:
needs:
- is-release-tag
- source-tarball
- packages-for-linux
name: Create release draft
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Workaround for https://github.com/actions/checkout/issues/882
ref: ${{ inputs.version }}
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
merge-multiple: true
path: /tmp/artifacts
- name: List artifacts
run: find /tmp/artifacts -type f
- name: Create draft release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.version }}
name: fish ${{ inputs.version }}
body_path: /tmp/artifacts/release-notes.md
draft: true
files: |
/tmp/artifacts/fish-${{ inputs.version }}.tar.xz
/tmp/artifacts/fish-${{ inputs.version }}-linux-*.tar.xz
packages-for-macos:
needs: [is-release-tag, create-draft-release]
name: Build packages for macOS
runs-on: macos-latest
environment: macos-codesign
steps:
- uses: actions/checkout@v4
with:
# Workaround for https://github.com/actions/checkout/issues/882
ref: ${{ inputs.version }}
- name: Install Rust
uses: ./.github/actions/rust-toolchain@oldest-supported
with:
targets: x86_64-apple-darwin
- name: Install Rust Stable
uses: ./.github/actions/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Build and codesign
run: |
die() { echo >&2 "$*"; exit 1; }
[ -n "$MAC_CODESIGN_APP_P12_BASE64" ] || die "Missing MAC_CODESIGN_APP_P12_BASE64"
[ -n "$MAC_CODESIGN_INSTALLER_P12_BASE64" ] || die "Missing MAC_CODESIGN_INSTALLER_P12_BASE64"
[ -n "$MAC_CODESIGN_PASSWORD" ] || die "Missing MAC_CODESIGN_PASSWORD"
[ -n "$MACOS_NOTARIZE_JSON" ] || die "Missing MACOS_NOTARIZE_JSON"
set -x
export FISH_ARTEFACT_PATH=/tmp/fish-built
# macOS runners keep having issues loading Cargo.toml dependencies from git (GitHub) instead
# of crates.io, so give this a try. It's also sometimes significantly faster on all platforms.
export CARGO_NET_GIT_FETCH_WITH_CLI=true
cargo install apple-codesign
mkdir -p "$FISH_ARTEFACT_PATH"
echo "$MAC_CODESIGN_APP_P12_BASE64" | base64 --decode >/tmp/app.p12
echo "$MAC_CODESIGN_INSTALLER_P12_BASE64" | base64 --decode >/tmp/installer.p12
echo "$MACOS_NOTARIZE_JSON" >/tmp/notarize.json
./build_tools/make_macos_pkg.sh -s -f /tmp/app.p12 \
-i /tmp/installer.p12 -p "$MAC_CODESIGN_PASSWORD" \
-n -j /tmp/notarize.json
version=$(git describe)
[ -f "${FISH_ARTEFACT_PATH}/fish-$version.app.zip" ]
[ -f "${FISH_ARTEFACT_PATH}/fish-$version.pkg" ]
rm /tmp/installer.p12 /tmp/app.p12 /tmp/notarize.json
env:
MAC_CODESIGN_APP_P12_BASE64: ${{ secrets.MAC_CODESIGN_APP_P12_BASE64 }}
MAC_CODESIGN_INSTALLER_P12_BASE64: ${{ secrets.MAC_CODESIGN_INSTALLER_P12_BASE64 }}
MAC_CODESIGN_PASSWORD: ${{ secrets.MAC_CODESIGN_PASSWORD }}
MACOS_NOTARIZE_JSON: ${{ secrets.MACOS_NOTARIZE_JSON }}
- name: Add macOS packages to the release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
version=$(git describe)
gh release upload $version \
/tmp/fish-built/fish-$version.app.zip \
/tmp/fish-built/fish-$version.pkg

View File

@@ -1,47 +0,0 @@
name: staticbuilds
on:
# release:
# types: [published]
# schedule:
# - cron: "14 13 * * *"
workflow_dispatch:
env:
CTEST_PARALLEL_LEVEL: "1"
CMAKE_BUILD_PARALLEL_LEVEL: "4"
jobs:
staticbuilds:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: dtolnay/rust-toolchain@1.70
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Prepare
run: |
sudo apt install python3-sphinx
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-musl
sudo apt install musl-tools crossbuild-essential-arm64 -y
- name: Build
run: |
CFLAGS="$CFLAGS -D_FORTIFY_SOURCE=2" CMAKE_WITH_GETTEXT=0 CC=aarch64-linux-gnu-gcc RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc -C link-arg=-lgcc -C link-arg=-D_FORTIFY_SOURCE=0" cargo build --release --target aarch64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
- name: Compress
run: |
tar -cazf fish-amd64.tar.xz -C target/x86_64-unknown-linux-musl/release/ fish{,_indent,_key_reader}
tar -cazf fish-aarch64.tar.xz -C target/aarch64-unknown-linux-musl/release/ fish{,_indent,_key_reader}
- uses: actions/upload-artifact@v4
with:
name: fish
path: |
fish-amd64.tar.xz
fish-aarch64.tar.xz
retention-days: 14

View File

@@ -1,3 +1,57 @@
fish 4.0.9 (released September 27, 2025)
========================================
This release fixes:
- a regression in 4.0.6 causing shifted keys to not be inserted on some terminals (:issue:`11813`).
- a regression in 4.0.6 causing the build to fail on systems where ``char`` is unsigned (:issue:`11804`).
- a regression in 4.0.0 causing a crash on an invalid :doc:`bg <cmds/bg>` invocation.
--------------
fish 4.0.8 (released September 18, 2025)
========================================
This release fixes a regression in 4.0.6 that caused user bindings to be shadowed by either fish's or a plugin's bindings (:issue:`11803`).
--------------
fish 4.0.6 (released September 12, 2025)
========================================
This release of fish fixes a number of issues identified in fish 4.0.2:
- fish now properly inherits $PATH under Windows WSL2 (:issue:`11354`).
- Remote filesystems are detected properly again on non-Linux systems.
- the :doc:`printf <cmds/printf>` builtin no longer miscalculates width of multi-byte characters (:issue:`11412`).
- For many years, fish has been "relocatable" -- it was possible to move the entire ``CMAKE_INSTALL_PREFIX`` and fish would use paths relative to its binary.
Only gettext locale paths were still determined purely at compile time, which has been fixed.
- the :doc:`commandline <cmds/commandline>` builtin failed to print the commandline set by a ``commandline -C`` invocation, which broke some completion scripts.
This has been corrected (:issue:`11423`).
- To work around terminals that fail to parse Operating System Command (OSC) sequences, a temporary feature flag has been added.
It allows you to disable prompt marking (OSC 133) by running (once) ``set -Ua fish_features no-mark-prompt`` and restarting fish (:issue:`11749`).
- The routines to save history and universal variables have seen some robustness improvements.
- builtin :doc:`status current-command <cmds/status>` no longer prints a trailing blank line.
- A crash displaying multi-line quoted command substitutions has been fixed (:issue:`11444`).
- Commands like ``set fish_complete_path ...`` accidentally disabled completion autoloading, which has been corrected.
- ``nmcli`` completions have been fixed to query network information dynamically instead of only when completing the first time.
- Git completions no longer print an error when no `git-foo` executable is in :envvar:`PATH`.
- Custom completions like ``complete foo -l long -xa ...`` that use the output of ``commandline -t``.
on a command-line like ``foo --long=`` have been invalidated by a change in 4.0; the completion scripts have been adjusted accordingly (:issue:`11508`).
- Some completions were misinterpreted, which caused garbage to be displayed in the completion list. This has been fixed.
- fish no longer interprets invalid control sequences from the terminal as if they were :kbd:`alt-[` or :kbd:`alt-o` key strokes.
- :doc:`bind <cmds/bind>` has been taught about the :kbd:`printscreen` and :kbd:`menu` keys.
- :kbd:`alt-delete` now deletes the word right of the cursor.
- :kbd:`ctrl-alt-h` erases the last word again (:issue:`11548`).
- :kbd:`alt-left` :kbd:`alt-right` were misinterpreted because they send unexpected sequences on some terminals; a workaround has been added. (:issue:`11479`).
- Key bindings like ``bind shift-A`` are no longer accepted; use ``bind shift-a`` or ``bind A``.
- Key bindings like ``bind shift-a`` take precedence over ``bind A`` when the key event included the shift modifier.
- Bindings using shift with non-ASCII letters (such as :kbd:`ctrl-shift-ä`) are now supported.
- Bindings with modifiers such as ``bind ctrl-w`` work again on non-Latin keyboard layouts such as a Russian one.
This is implemented by allowing key events such as :kbd:`ctrl-ц` to match bindings of the corresponding Latin key, using the kitty keyboard protocol's base layout key (:issue:`11520`).
- Vi mode: The cursor position after pasting via :kbd:`p` has been corrected.
- Vi mode: Trying to replace the last character via :kbd:`r` no longer replaces the last-but-one character (:issue:`11484`),
fish 4.0.2 (released April 20, 2025)
====================================

16
Cargo.lock generated
View File

@@ -112,7 +112,7 @@ dependencies = [
[[package]]
name = "fish"
version = "4.0.2"
version = "4.0.9"
dependencies = [
"bitflags",
"cc",
@@ -139,6 +139,8 @@ name = "fish-printf"
version = "0.2.1"
dependencies = [
"libc",
"unicode-segmentation",
"unicode-width",
"widestring",
]
@@ -567,6 +569,18 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "version_check"
version = "0.9.5"

View File

@@ -16,7 +16,7 @@ debug = true
[package]
name = "fish"
version = "4.0.2"
version = "4.0.9"
edition.workspace = true
rust-version.workspace = true
default-run = "fish"

View File

@@ -227,7 +227,7 @@ fn has_small_stack(_: &Target) -> Result<bool, Box<dyn Error>> {
}
fn setup_paths() {
fn get_path(name: &str, default: &str, onvar: PathBuf) -> PathBuf {
fn get_path(name: &str, default: &str, onvar: &Path) -> PathBuf {
let mut var = PathBuf::from(env::var(name).unwrap_or(default.to_string()));
if var.is_relative() {
var = onvar.join(var);
@@ -250,7 +250,7 @@ fn get_path(name: &str, default: &str, onvar: PathBuf) -> PathBuf {
rsconf::rebuild_if_env_changed("PREFIX");
rsconf::set_env_value("PREFIX", prefix.to_str().unwrap());
let datadir = get_path("DATADIR", "share/", prefix.clone());
let datadir = get_path("DATADIR", "share/", &prefix);
rsconf::set_env_value("DATADIR", datadir.to_str().unwrap());
rsconf::rebuild_if_env_changed("DATADIR");
@@ -261,7 +261,7 @@ fn get_path(name: &str, default: &str, onvar: PathBuf) -> PathBuf {
};
rsconf::set_env_value("DATADIR_SUBDIR", datadir_subdir);
let bindir = get_path("BINDIR", "bin/", prefix.clone());
let bindir = get_path("BINDIR", "bin/", &prefix);
rsconf::set_env_value("BINDIR", bindir.to_str().unwrap());
rsconf::rebuild_if_env_changed("BINDIR");
@@ -270,16 +270,16 @@ fn get_path(name: &str, default: &str, onvar: PathBuf) -> PathBuf {
// If we get our prefix from $HOME, we should use the system's /etc/
// ~/.local/share/etc/ makes no sense
if prefix_from_home { "/etc/" } else { "etc/" },
datadir.clone(),
&datadir,
);
rsconf::set_env_value("SYSCONFDIR", sysconfdir.to_str().unwrap());
rsconf::rebuild_if_env_changed("SYSCONFDIR");
let localedir = get_path("LOCALEDIR", "locale/", datadir.clone());
let localedir = get_path("LOCALEDIR", "locale/", &datadir);
rsconf::set_env_value("LOCALEDIR", localedir.to_str().unwrap());
rsconf::rebuild_if_env_changed("LOCALEDIR");
let docdir = get_path("DOCDIR", "doc/fish", datadir.clone());
let docdir = get_path("DOCDIR", "doc/fish", &datadir);
rsconf::set_env_value("DOCDIR", docdir.to_str().unwrap());
rsconf::rebuild_if_env_changed("DOCDIR");
}
@@ -292,7 +292,7 @@ fn get_version(src_dir: &Path) -> String {
return var;
}
let path = PathBuf::from(src_dir).join("version");
let path = src_dir.join("version");
if let Ok(strver) = read_to_string(path) {
return strver.to_string();
}
@@ -321,7 +321,7 @@ fn get_version(src_dir: &Path) -> String {
// or because it refused (safe.directory applies to `git describe`!)
// So we read the SHA ourselves.
fn get_git_hash() -> Result<String, Box<dyn std::error::Error>> {
let gitdir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".git");
let gitdir = Path::new(env!("CARGO_MANIFEST_DIR")).join(".git");
// .git/HEAD contains ref: refs/heads/branch
let headpath = gitdir.join("HEAD");

177
build_tools/make_macos_pkg.sh Executable file
View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# Script to produce an OS X installer .pkg and .app(.zip)
usage() {
echo "Build macOS packages, optionally signing and notarizing them."
echo "Usage: $0 options"
echo "Options:"
echo " -s Enables code signing"
echo " -f <APP_KEY.p12> Path to .p12 file for application signing"
echo " -i <INSTALLER_KEY.p12> Path to .p12 file for installer signing"
echo " -p <PASSWORD> Password for the .p12 files (necessary to access the certificates)"
echo " -e <entitlements file> (Optional) Path to an entitlements XML file"
echo " -n Enables notarization. This will fail if code signing is not also enabled."
echo " -j <API_KEY.JSON> Path to JSON file generated with \`rcodesign encode-app-store-connect-api-key\` (required for notarization)"
echo
exit 1
}
set -x
set -e
SIGN=
NOTARIZE=
ARM64_DEPLOY_TARGET='MACOSX_DEPLOYMENT_TARGET=11.0'
X86_64_DEPLOY_TARGET='MACOSX_DEPLOYMENT_TARGET=10.9'
# As of this writing, the most recent Rust release supports macOS back to 10.12.
# The first supported version of macOS on arm64 is 10.15, so any Rust is fine for arm64.
# We wish to support back to 10.9 on x86-64; the last version of Rust to support that is
# version 1.73.0.
RUST_VERSION_X86_64=1.70.0
while getopts "sf:i:p:e:nj:" opt; do
case $opt in
s) SIGN=1;;
f) P12_APP_FILE=$(realpath "$OPTARG");;
i) P12_INSTALL_FILE=$(realpath "$OPTARG");;
p) P12_PASSWORD="$OPTARG";;
e) ENTITLEMENTS_FILE=$(realpath "$OPTARG");;
n) NOTARIZE=1;;
j) API_KEY_FILE=$(realpath "$OPTARG");;
\?) usage;;
esac
done
if [ -n "$SIGN" ] && { [ -z "$P12_APP_FILE" ] || [ -z "$P12_INSTALL_FILE" ] || [ -z "$P12_PASSWORD" ]; }; then
usage
fi
if [ -n "$NOTARIZE" ] && [ -z "$API_KEY_FILE" ]; then
usage
fi
VERSION=$(build_tools/git_version_gen.sh --stdout 2>/dev/null)
echo "Version is $VERSION"
PKGDIR=$(mktemp -d)
echo "$PKGDIR"
SRC_DIR=$PWD
OUTPUT_PATH=${FISH_ARTEFACT_PATH:-~/fish_built}
mkdir -p "$PKGDIR/build_x86_64" "$PKGDIR/build_arm64" "$PKGDIR/root" "$PKGDIR/intermediates" "$PKGDIR/dst"
# Build and install for arm64.
# Pass FISH_USE_SYSTEM_PCRE2=OFF because a system PCRE2 on macOS will not be signed by fish,
# and will probably not be built universal, so the package will fail to validate/run on other systems.
# Note CMAKE_OSX_ARCHITECTURES is still relevant for the Mac app.
{ cd "$PKGDIR/build_arm64" \
&& cmake \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXE_LINKER_FLAGS="-Wl,-ld_classic" \
-DWITH_GETTEXT=OFF \
-DRust_CARGO_TARGET=aarch64-apple-darwin \
-DCMAKE_OSX_ARCHITECTURES='arm64;x86_64' \
-DFISH_USE_SYSTEM_PCRE2=OFF \
"$SRC_DIR" \
&& env $ARM64_DEPLOY_TARGET make VERBOSE=1 -j 12 \
&& env DESTDIR="$PKGDIR/root/" $ARM64_DEPLOY_TARGET make install;
}
# Build for x86-64 but do not install; instead we will make some fat binaries inside the root.
# Set RUST_VERSION_X86_64 to the last version of Rust that supports macOS 10.9.
{ cd "$PKGDIR/build_x86_64" \
&& cmake \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXE_LINKER_FLAGS="-Wl,-ld_classic" \
-DWITH_GETTEXT=OFF \
-DRust_TOOLCHAIN="$RUST_VERSION_X86_64" \
-DRust_CARGO_TARGET=x86_64-apple-darwin \
-DCMAKE_OSX_ARCHITECTURES='arm64;x86_64' \
-DFISH_USE_SYSTEM_PCRE2=OFF "$SRC_DIR" \
&& env $X86_64_DEPLOY_TARGET make VERBOSE=1 -j 12; }
# Fatten them up.
for FILE in "$PKGDIR"/root/usr/local/bin/*; do
X86_FILE="$PKGDIR/build_x86_64/$(basename "$FILE")"
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
chmod 755 "$FILE"
done
if test -n "$SIGN"; then
echo "Signing executables"
ARGS=(
--p12-file "$P12_APP_FILE"
--p12-password "$P12_PASSWORD"
--code-signature-flags runtime
--for-notarization
)
if [ -n "$ENTITLEMENTS_FILE" ]; then
ARGS+=(--entitlements-xml-file "$ENTITLEMENTS_FILE")
fi
for FILE in "$PKGDIR"/root/usr/local/bin/*; do
(set +x; rcodesign sign "${ARGS[@]}" "$FILE")
done
fi
pkgbuild --scripts "$SRC_DIR/build_tools/osx_package_scripts" --root "$PKGDIR/root/" --identifier 'com.ridiculousfish.fish-shell-pkg' --version "$VERSION" "$PKGDIR/intermediates/fish.pkg"
productbuild --package-path "$PKGDIR/intermediates" --distribution "$SRC_DIR/build_tools/osx_distribution.xml" --resources "$SRC_DIR/build_tools/osx_package_resources/" "$OUTPUT_PATH/fish-$VERSION.pkg"
if test -n "$SIGN"; then
echo "Signing installer"
ARGS=(
--p12-file "$P12_INSTALL_FILE"
--p12-password "$P12_PASSWORD"
--code-signature-flags runtime
--for-notarization
)
(set +x; rcodesign sign "${ARGS[@]}" "$OUTPUT_PATH/fish-$VERSION.pkg")
fi
# Make the app
(cd "$PKGDIR/build_arm64" && env $ARM64_DEPLOY_TARGET make -j 12 fish_macapp)
(cd "$PKGDIR/build_x86_64" && env $X86_64_DEPLOY_TARGET make -j 12 fish_macapp)
# Make the app's /usr/local/bin binaries universal. Note fish.app/Contents/MacOS/fish already is, courtesy of CMake.
cd "$PKGDIR/build_arm64"
for FILE in fish.app/Contents/Resources/base/usr/local/bin/*; do
X86_FILE="$PKGDIR/build_x86_64/fish.app/Contents/Resources/base/usr/local/bin/$(basename "$FILE")"
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
# macho-universal-create screws up the permissions.
chmod 755 "$FILE"
done
if test -n "$SIGN"; then
echo "Signing app"
ARGS=(
--p12-file "$P12_APP_FILE"
--p12-password "$P12_PASSWORD"
--code-signature-flags runtime
--for-notarization
)
if [ -n "$ENTITLEMENTS_FILE" ]; then
ARGS+=(--entitlements-xml-file "$ENTITLEMENTS_FILE")
fi
(set +x; rcodesign sign "${ARGS[@]}" "fish.app")
fi
cp -R "fish.app" "$OUTPUT_PATH/fish-$VERSION.app"
cd "$OUTPUT_PATH"
# Maybe notarize.
if test -n "$NOTARIZE"; then
echo "Notarizing"
rcodesign notarize --staple --wait --max-wait-seconds 1800 --api-key-file "$API_KEY_FILE" "$OUTPUT_PATH/fish-$VERSION.pkg"
rcodesign notarize --staple --wait --max-wait-seconds 1800 --api-key-file "$API_KEY_FILE" "$OUTPUT_PATH/fish-$VERSION.app"
fi
# Zip it up.
zip -r "fish-$VERSION.app.zip" "fish-$VERSION.app" && rm -Rf "fish-$VERSION.app"
rm -rf "$PKGDIR"

View File

@@ -3,18 +3,18 @@
# Script to produce an OS X installer .pkg and .app(.zip)
usage() {
echo "Build macOS packages, optionally signing and notarizing them."
echo "Usage: $0 options"
echo "Options:"
echo " -s Enables code signing"
echo " -f <APP_KEY.p12> Path to .p12 file for application signing"
echo " -i <INSTALLER_KEY.p12> Path to .p12 file for installer signing"
echo " -p <PASSWORD> Password for the .p12 files (necessary to access the certificates)"
echo " -e <entitlements file> (Optional) Path to an entitlements XML file"
echo " -n Enables notarization. This will fail if code signing is not also enabled."
echo " -j <API_KEY.JSON> Path to JSON file generated with `rcodesign encode-app-store-connect-api-key` (required for notarization)"
echo
exit 1
echo "Build macOS packages, optionally signing and notarizing them."
echo "Usage: $0 options"
echo "Options:"
echo " -s Enables code signing"
echo " -f <APP_KEY.p12> Path to .p12 file for application signing"
echo " -i <INSTALLER_KEY.p12> Path to .p12 file for installer signing"
echo " -p <PASSWORD> Password for the .p12 files (necessary to access the certificates)"
echo " -e <entitlements file> (Optional) Path to an entitlements XML file"
echo " -n Enables notarization. This will fail if code signing is not also enabled."
echo " -j <API_KEY.JSON> Path to JSON file generated with \`rcodesign encode-app-store-connect-api-key\` (required for notarization)"
echo
exit 1
}
set -x
@@ -30,35 +30,35 @@ X86_64_DEPLOY_TARGET='MACOSX_DEPLOYMENT_TARGET=10.9'
# The first supported version of macOS on arm64 is 10.15, so any Rust is fine for arm64.
# We wish to support back to 10.9 on x86-64; the last version of Rust to support that is
# version 1.73.0.
RUST_VERSION_X86_64=1.73.0
RUST_VERSION_X86_64=1.70.0
while getopts "sf:i:p:e:nj:" opt; do
case $opt in
s) SIGN=1;;
f) P12_APP_FILE=$(realpath "$OPTARG");;
i) P12_INSTALL_FILE=$(realpath "$OPTARG");;
p) P12_PASSWORD="$OPTARG";;
e) ENTITLEMENTS_FILE=$(realpath "$OPTARG");;
n) NOTARIZE=1;;
j) API_KEY_FILE=$(realpath "$OPTARG");;
\?) usage;;
esac
case $opt in
s) SIGN=1;;
f) P12_APP_FILE=$(realpath "$OPTARG");;
i) P12_INSTALL_FILE=$(realpath "$OPTARG");;
p) P12_PASSWORD="$OPTARG";;
e) ENTITLEMENTS_FILE=$(realpath "$OPTARG");;
n) NOTARIZE=1;;
j) API_KEY_FILE=$(realpath "$OPTARG");;
\?) usage;;
esac
done
if [ -n "$SIGN" ] && ([ -z "$P12_APP_FILE" ] || [-z "$P12_INSTALL_FILE"] || [ -z "$P12_PASSWORD" ]); then
usage
if [ -n "$SIGN" ] && { [ -z "$P12_APP_FILE" ] || [ -z "$P12_INSTALL_FILE" ] || [ -z "$P12_PASSWORD" ]; }; then
usage
fi
if [ -n "$NOTARIZE" ] && [ -z "$API_KEY_FILE" ]; then
usage
usage
fi
VERSION=$(git describe --always --dirty 2>/dev/null)
if test -z "$VERSION" ; then
echo "Could not get version from git"
if test -f version; then
VERSION=$(cat version)
fi
echo "Could not get version from git"
if test -f version; then
VERSION=$(cat version)
fi
fi
echo "Version is $VERSION"
@@ -76,7 +76,7 @@ mkdir -p "$PKGDIR/build_x86_64" "$PKGDIR/build_arm64" "$PKGDIR/root" "$PKGDIR/in
# and will probably not be built universal, so the package will fail to validate/run on other systems.
# Note CMAKE_OSX_ARCHITECTURES is still relevant for the Mac app.
{ cd "$PKGDIR/build_arm64" \
&& cmake \
&& cmake \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXE_LINKER_FLAGS="-Wl,-ld_classic" \
-DWITH_GETTEXT=OFF \
@@ -91,7 +91,7 @@ mkdir -p "$PKGDIR/build_x86_64" "$PKGDIR/build_arm64" "$PKGDIR/root" "$PKGDIR/in
# Build for x86-64 but do not install; instead we will make some fat binaries inside the root.
# Set RUST_VERSION_X86_64 to the last version of Rust that supports macOS 10.9.
{ cd "$PKGDIR/build_x86_64" \
&& cmake \
&& cmake \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXE_LINKER_FLAGS="-Wl,-ld_classic" \
-DWITH_GETTEXT=OFF \
@@ -99,11 +99,11 @@ mkdir -p "$PKGDIR/build_x86_64" "$PKGDIR/build_arm64" "$PKGDIR/root" "$PKGDIR/in
-DRust_CARGO_TARGET=x86_64-apple-darwin \
-DCMAKE_OSX_ARCHITECTURES='arm64;x86_64' \
-DFISH_USE_SYSTEM_PCRE2=OFF "$SRC_DIR" \
&& env $X86_64_DEPLOY_TARGET make VERBOSE=1 -j 12; }
&& env $X86_64_DEPLOY_TARGET make VERBOSE=1 -j 12; }
# Fatten them up.
for FILE in "$PKGDIR"/root/usr/local/bin/*; do
X86_FILE="$PKGDIR/build_x86_64/$(basename $FILE)"
X86_FILE="$PKGDIR/build_x86_64/$(basename "$FILE")"
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
chmod 755 "$FILE"
done
@@ -145,7 +145,7 @@ fi
# Make the app's /usr/local/bin binaries universal. Note fish.app/Contents/MacOS/fish already is, courtesy of CMake.
cd "$PKGDIR/build_arm64"
for FILE in fish.app/Contents/Resources/base/usr/local/bin/*; do
X86_FILE="$PKGDIR/build_x86_64/fish.app/Contents/Resources/base/usr/local/bin/$(basename $FILE)"
X86_FILE="$PKGDIR/build_x86_64/fish.app/Contents/Resources/base/usr/local/bin/$(basename "$FILE")"
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
# macho-universal-create screws up the permissions.

104
build_tools/release-notes.sh Executable file
View File

@@ -0,0 +1,104 @@
#!/bin/sh
set -e
workspace_root=$(dirname "$0")/..
relnotes_tmp=$(mktemp -d)
mkdir -p "$relnotes_tmp/fake-workspace" "$relnotes_tmp/out"
(
cd "$workspace_root"
cp -r doc_src CONTRIBUTING.rst README.rst "$relnotes_tmp/fake-workspace"
)
version=$(sed 's,^fish \(\S*\) .*,\1,; 1q' "$workspace_root/CHANGELOG.rst")
previous_version=$(
cd "$workspace_root"
awk <CHANGELOG.rst '
( /^fish \S*\.\S*\.\S* \(released .*\)$/ &&
NR > 1 &&
# Skip tags that have not been created yet..
system("git rev-parse --verify >/dev/null --quiet refs/tags/"$2) == 0 \
) {
print $2; ok = 1; exit
}
END { exit !ok }
'
)
minor_version=${version%.*}
previous_minor_version=${previous_version%.*}
{
sed -n 1,2p <"$workspace_root/CHANGELOG.rst"
ListCommitters() {
comm "$@" "$relnotes_tmp/committers-then" "$relnotes_tmp/committers-now"
}
(
cd "$workspace_root"
git log "$previous_version" --format="%aN" | sort -u >"$relnotes_tmp/committers-then"
git log "$previous_version".. --format="%aN" | sort -u >"$relnotes_tmp/committers-now"
ListCommitters -13 >"$relnotes_tmp/committers-new"
ListCommitters -12 >"$relnotes_tmp/committers-returning"
)
if [ "$minor_version" != "$previous_minor_version" ]; then
(
cd "$workspace_root"
num_commits=$(git log --no-merges --format=%H "$previous_version".. | wc -l)
num_authors=$(wc -l <"$relnotes_tmp/committers-now")
num_new_authors=$(wc -l <"$relnotes_tmp/committers-new")
printf %s \
"This release comprises $num_commits commits since $previous_version," \
" contributed by $num_authors authors, $num_new_authors of which are new committers."
echo
echo
)
fi
printf %s "$(awk <"$workspace_root/CHANGELOG.rst" '
NR <= 2 || /^\.\. ignore / { next }
/^===/ { exit }
{ print }
' | sed '$d')" |
sed -e '$s/^----*$//' # Remove spurious transitions at the end of the document.
if [ "$minor_version" != "$previous_minor_version" ]; then
JoinEscaped() {
sed 's/\S/\\&/g' |
awk '
NR != 1 { printf ",\n" }
{ printf "%s", $0 }
END { printf "\n" }
'
}
echo ""
echo "---"
echo ""
echo "Thanks to everyone who contributed through issue discussions, code reviews, or code changes."
echo
printf "Welcome our new committers: "
JoinEscaped <"$relnotes_tmp/committers-new"
echo
printf "Welcome back our returning committers: "
JoinEscaped <"$relnotes_tmp/committers-returning"
fi
echo
echo "---"
echo
echo "*Download links: To download the source code for fish, we suggest the file named \"fish-$version.tar.xz\". The file downloaded from \"Source code (tar.gz)\" will not build correctly.*"
echo
echo "*The files called fish-$version-linux-\*.tar.xz are experimental packages containing a single standalone ``fish`` binary for any Linux with the given CPU architecture.*"
} >"$relnotes_tmp/fake-workspace"/CHANGELOG.rst
sphinx-build >&2 -j auto \
-W -E -b markdown -c "$workspace_root/doc_src" \
-d "$relnotes_tmp/doctree" "$relnotes_tmp/fake-workspace/doc_src" "$relnotes_tmp/out" \
-D markdown_http_base="https://fishshell.com/docs/$minor_version" \
-D markdown_uri_doc_suffix=".html" \
-D markdown_github_flavored=1 \
"$@"
# Skip changelog header
sed -n 1p "$relnotes_tmp/out/relnotes.md" | grep -Fxq "# Release notes"
sed -n 2p "$relnotes_tmp/out/relnotes.md" | grep -Fxq ''
sed 1,2d "$relnotes_tmp/out/relnotes.md"
rm -r "$relnotes_tmp"

229
build_tools/release.sh Executable file
View File

@@ -0,0 +1,229 @@
#!/bin/sh
{
set -ex
version=$1
repository_owner=fish-shell
remote=origin
if [ -n "$2" ]; then
set -u
repository_owner=$2
remote=$3
set +u
[ $# -eq 3 ]
fi
[ -n "$version" ]
for tool in \
bundle \
gh \
jq \
ruby \
timeout \
; do
if ! command -v "$tool" >/dev/null; then
echo >&2 "$0: missing command: $1"
exit 1
fi
done
repo_root="$(dirname "$0")/.."
fish_site=$repo_root/../fish-site
for path in . "$fish_site"
do
if ! git -C "$path" diff HEAD --quiet ||
git ls-files --others --exclude-standard | grep .; then
echo >&2 "$0: index and worktree must be clean"
exit 1
fi
done
if git tag | grep -qxF "$version"; then
echo >&2 "$0: tag $version already exists"
exit 1
fi
integration_branch=$(
git for-each-ref --points-at=HEAD 'refs/heads/Integration_*' \
--format='%(refname:strip=2)'
)
[ -n "$integration_branch" ] ||
git merge-base --is-ancestor $remote/master HEAD
sed -n 1p CHANGELOG.rst | grep -q '^fish .*(released .*)$'
sed -n 2p CHANGELOG.rst | grep -q '^===*$'
changelog_title="fish $version (released $(date +'%B %d, %Y'))"
sed -i \
-e "1c$changelog_title" \
-e "2c$(printf %s "$changelog_title" | sed s/./=/g)" \
CHANGELOG.rst
CommitVersion() {
sed -i "s/^version = \".*\"/version = \"$1\"/g" Cargo.toml
cargo fetch --offline
git add CHANGELOG.rst Cargo.toml Cargo.lock
git commit -m "$2
Created by ./build_tools/release.sh $version"
}
CommitVersion "$version" "Release $version"
# N.B. this is not GPG-signed.
git tag --annotate --message="Release $version" $version
git push $remote $version
TIMEOUT=
gh() {
command ${TIMEOUT:+timeout $TIMEOUT} \
gh --repo "$repository_owner/fish-shell" "$@"
}
gh workflow run release.yml --ref="$version" \
--raw-field="version=$version"
run_id=
while [ -z "$run_id" ] && sleep 5
do
run_id=$(gh run list \
--json=databaseId --jq=.[].databaseId \
--workflow=release.yml --limit=1 \
--commit="$(git rev-parse "$version^{commit}")")
done
# Update fishshell.com
tag_oid=$(git rev-parse "$version")
tmpdir=$(mktemp -d)
# TODO This works on draft releases only if "gh" is configured to
# have write access to the fish-shell repository. Unless we are fine
# publishing the release at this point, we should at least fail if
# "gh" doesn't have write access.
while ! \
gh release download "$version" --dir="$tmpdir" \
--pattern="fish-$version.tar.xz"
do
TIMEOUT=30 gh run watch "$run_id" ||:
sleep 5
done
actual_tag_oid=$(git ls-remote "$remote" |
awk '$2 == "refs/tags/'"$version"'" { print $1 }')
[ "$tag_oid" = "$actual_tag_oid" ]
( cd "$tmpdir" && tar xf fish-$version.tar.xz )
CopyDocs() {
rm -rf "$fish_site/site/docs/$1"
cp -r "$tmpdir/fish-$version/user_doc/html" "$fish_site/site/docs/$1"
git -C $fish_site add "site/docs/$1"
}
minor_version=${version%.*}
CopyDocs "$minor_version"
latest_release=$(
releases=$(git tag | grep '^[0-9]*\.[0-9]*\.[0-9]*.*' |
sed $(: "De-prioritize release candidates (1.2.3-rc0)") \
's/-/~/g' | LC_ALL=C sort --version-sort)
printf %s\\n "$releases" | tail -1
)
if [ "$version" = "$latest_release" ]; then
CopyDocs current
fi
rm -rf "$tmpdir"
(
cd "$fish_site"
make
git add -u
! git ls-files --others --exclude-standard | grep .
git commit --message="$(printf %s "\
| Release $version (docs)
|
| Created by ../fish-shell/build_tools/release.sh
" | sed 's,^\s*| \?,,')"
)
# Approve macos-codesign
# TODO what if current user can't approve?
gh_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)
[ -z "$environment_id" ]
}
do
sleep 5
done
echo '
{
"environment_ids": ['"$environment_id"'],
"state": "approved",
"comment": "Approved via ./build_tools/release.sh"
}
' |
gh_pending_deployments -XPOST --input=-
# Await completion.
gh run watch "$run_id"
while {
! draft=$(gh release view "$version" --json=isDraft --jq=.isDraft) \
|| [ "$draft" = true ]
}
do
sleep 20
done
(
cd "$fish_site"
make new-release
git add -u
! git ls-files --others --exclude-standard | grep .
git commit --message="$(printf %s "\
| Release $version (release list update)
|
| Created by ../fish-shell/build_tools/release.sh
" | sed 's,^\s*| \?,,')"
# This takes care to support remote names that are different from
# fish-shell remote name. Also, support detached HEAD state.
git push git@github.com:$repository_owner/fish-site HEAD:master
)
if [ -n "$integration_branch" ]; then
git push $remote "$version^{commit}":refs/heads/$integration_branch
else
changelog=$(cat - CHANGELOG.rst <<EOF
fish ?.?.? (released ???)
=========================
EOF
)
printf %s\\n "$changelog" >CHANGELOG.rst
CommitVersion ${version}-snapshot "start new cycle"
git push $remote HEAD:master
fi
# TODO This can currently require a TTY for editing and password
# prompts.
if [ "$repository_owner" = fish-shell ]; then {
mail=$(mktemp)
cat >$mail <<EOF
From: $(git var GIT_AUTHOR_IDENT | sed 's/ [0-9]* +[0-9]*$//')
Subject: fish $version released
See https://github.com/fish-shell/fish-shell/releases/tag/$version
EOF
git send-email --suppress-cc=all --confirm=always $mail \
--to="fish-users Mailing List <fish-users@lists.sourceforge.net>"
rm $mail
} fi
exit
}

View File

@@ -24,7 +24,7 @@ add_executable(fish_macapp EXCLUDE_FROM_ALL
# Compute the version. Note this is done at generation time, not build time,
# so cmake must be re-run after version changes for the app to be updated. But
# generally this will be run by make_pkg.sh which always re-runs cmake.
# generally this will be run by make_macos_pkg.sh which always re-runs cmake.
execute_process(
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build_tools/git_version_gen.sh --stdout
COMMAND cut -d- -f1
@@ -32,7 +32,7 @@ execute_process(
OUTPUT_STRIP_TRAILING_WHITESPACE)
# Note CMake appends .app, so the real output name will be fish.app.
# Note CMake appends .app, so the real output name will be fish.app.
# This target does not include the 'base' resource.
set_target_properties(fish_macapp PROPERTIES OUTPUT_NAME "fish")

1
debian/control vendored
View File

@@ -20,7 +20,6 @@ Vcs-Browser: https://github.com/fish-shell/fish-shell
Package: fish
Architecture: any
Depends: bsdextrautils,
Depends: bsdextrautils | bsdmainutils,
file,
gettext-base,

5
debian/rules vendored
View File

@@ -17,6 +17,9 @@ override_dh_auto_configure:
dh_auto_configure --buildsystem=cmake -- -DCMAKE_BUILD_TYPE=RelWithDebInfo
override_dh_clean:
dh_clean
dh_clean --exclude=Cargo.toml.orig
-unlink .cargo
-unlink vendor
override_dh_auto_test:
make fish_run_tests

View File

@@ -10,9 +10,19 @@ import glob
import os.path
import subprocess
import sys
from sphinx.highlighting import lexers
from sphinx.errors import SphinxWarning
from docutils import nodes
try:
import sphinx_markdown_builder
extensions = [
"sphinx_markdown_builder",
]
except ImportError:
pass
# -- Helper functions --------------------------------------------------------
@@ -35,11 +45,14 @@ def issue_role(name, rawtext, text, lineno, inliner, options=None, content=None)
return [link], []
def remove_fish_indent_lexer(app):
if app.builder.name in ("man", "markdown"):
del lexers["fish-docs-samples"]
# -- Load our extensions -------------------------------------------------
def setup(app):
# Our own pygments lexers
from sphinx.highlighting import lexers
this_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, this_dir)
from fish_indent_lexer import FishIndentLexer
@@ -52,6 +65,8 @@ def setup(app):
app.add_config_value("issue_url", default=None, rebuild="html")
app.add_role("issue", issue_role)
app.connect("builder-inited", remove_fish_indent_lexer)
# The default language to assume
highlight_language = "fish-docs-samples"
@@ -59,7 +74,7 @@ highlight_language = "fish-docs-samples"
# -- Project information -----------------------------------------------------
project = "fish-shell"
copyright = "2024, fish-shell developers"
copyright = "fish-shell developers"
author = "fish-shell developers"
issue_url = "https://github.com/fish-shell/fish-shell/issues"
@@ -72,7 +87,7 @@ elif "FISH_BUILD_VERSION" in os.environ:
ret = os.environ["FISH_BUILD_VERSION"]
else:
ret = subprocess.check_output(
("fish_indent", "--version"), stderr=subprocess.STDOUT
("../build_tools/git_version_gen.sh", "--stdout"), stderr=subprocess.STDOUT
).decode("utf-8")
# The full version, including alpha/beta/rc tags

View File

@@ -9,3 +9,5 @@ license = "MIT"
[dependencies]
libc = "0.2.155"
widestring = { version = "1.0.2", optional = true }
unicode-segmentation = "1.12.0"
unicode-width = "0.2.0"

View File

@@ -73,7 +73,7 @@ macro_rules! sprintf {
/// - `args`: Iterator over the arguments to format.
///
/// # Returns
/// A `Result` which is `Ok` containing the number of characters written on success, or an `Error`.
/// A `Result` which is `Ok` containing the width of the string written on success, or an `Error`.
///
/// # Example
///

View File

@@ -5,6 +5,8 @@
use std::fmt::{self, Write};
use std::mem;
use std::result::Result;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
#[cfg(feature = "widestring")]
use widestring::Utf32Str as wstr;
@@ -382,7 +384,7 @@ pub fn sprintf_locale(
}
// Read field width. We do not support $.
let width = if s.at(0) == Some('*') {
let desired_width = if s.at(0) == Some('*') {
let arg_width = args.next().ok_or(Error::MissingArg)?.as_sint()?;
s.advance_by(1);
if arg_width < 0 {
@@ -397,7 +399,7 @@ pub fn sprintf_locale(
};
// Optionally read precision. We do not support $.
let mut prec: Option<usize> = if s.at(0) == Some('.') && s.at(1) == Some('*') {
let mut desired_precision: Option<usize> = if s.at(0) == Some('.') && s.at(1) == Some('*') {
// "A negative precision is treated as though it were missing."
// Here we assume the precision is always signed.
s.advance_by(2);
@@ -410,7 +412,7 @@ pub fn sprintf_locale(
None
};
// Disallow precisions larger than i32::MAX, in keeping with C.
if prec.unwrap_or(0) > i32::MAX as usize {
if desired_precision.unwrap_or(0) > i32::MAX as usize {
return Err(Error::Overflow);
}
@@ -429,7 +431,7 @@ pub fn sprintf_locale(
// "If a precision is given with a numeric conversion (d, i, o, u, i, x, and X),
// the 0 flag is ignored." p is included here.
let spec_is_numeric = matches!(conv_spec, CS::d | CS::u | CS::o | CS::p | CS::x | CS::X);
if spec_is_numeric && prec.is_some() {
if spec_is_numeric && desired_precision.is_some() {
flags.zero_pad = false;
}
@@ -443,13 +445,22 @@ pub fn sprintf_locale(
CS::e | CS::f | CS::g | CS::a | CS::E | CS::F | CS::G | CS::A => {
// Floating point types handle output on their own.
let float = arg.as_float()?;
let len = format_float(f, float, width, prec, flags, locale, conv_spec, buf)?;
let len = format_float(
f,
float,
desired_width,
desired_precision,
flags,
locale,
conv_spec,
buf,
)?;
out_len = out_len.checked_add(len).ok_or(Error::Overflow)?;
continue 'main;
}
CS::p => {
const PTR_HEX_DIGITS: usize = 2 * mem::size_of::<*const u8>();
prec = prec.map(|p| p.max(PTR_HEX_DIGITS));
desired_precision = desired_precision.map(|p| p.max(PTR_HEX_DIGITS));
let uint = arg.as_uint()?;
if uint != 0 {
prefix = "0x";
@@ -479,8 +490,8 @@ pub fn sprintf_locale(
if uint != 0 {
write!(buf, "{:o}", uint)?;
}
if flags.alt_form && prec.unwrap_or(0) <= buf.len() + 1 {
prec = Some(buf.len() + 1);
if flags.alt_form && desired_precision.unwrap_or(0) <= buf.len() + 1 {
desired_precision = Some(buf.len() + 1);
}
buf
}
@@ -514,10 +525,38 @@ pub fn sprintf_locale(
CS::s => {
// also 'S'
let s = arg.as_str(buf)?;
let p = prec.unwrap_or(s.len()).min(s.len());
prec = Some(p);
flags.zero_pad = false;
&s[..p]
match desired_precision {
Some(precision) => {
// from man printf(3)
// "the maximum number of characters to be printed from a string"
// We interpret this to mean the maximum width when printed, as defined by
// Unicode grapheme cluster width.
let mut byte_len = 0;
let mut width = 0;
let mut graphemes = s.graphemes(true);
// Iteratively add single grapheme clusters as long as the fit within the
// width limited by precision.
while width < precision {
match graphemes.next() {
Some(grapheme) => {
let grapheme_width = grapheme.width();
if width + grapheme_width <= precision {
byte_len += grapheme.len();
width += grapheme_width;
} else {
break;
}
}
None => break,
}
}
let p = precision.min(width);
desired_precision = Some(p);
&s[..byte_len]
}
None => s,
}
}
};
// Numeric output should be empty iff the value is 0.
@@ -528,23 +567,26 @@ pub fn sprintf_locale(
// Decide if we want to apply thousands grouping to the body, and compute its size.
// Note we have already errored out if grouped is set and this is non-numeric.
let wants_grouping = flags.grouped && locale.thousands_sep.is_some();
let body_len = match wants_grouping {
let body_width = match wants_grouping {
// We assume that text representing numbers is ASCII, so len == width.
true => body.len() + locale.separator_count(body.len()),
false => body.len(),
false => body.width(),
};
// Resolve the precision.
// In the case of a non-numeric conversion, update the precision to at least the
// length of the string.
let prec = if !spec_is_numeric {
prec.unwrap_or(body_len)
let desired_precision = if !spec_is_numeric {
desired_precision.unwrap_or(body_width)
} else {
prec.unwrap_or(1).max(body_len)
desired_precision.unwrap_or(1).max(body_width)
};
let prefix_len = prefix.len();
let unpadded_width = prefix_len.checked_add(prec).ok_or(Error::Overflow)?;
let width = width.max(unpadded_width);
let prefix_width = prefix.width();
let unpadded_width = prefix_width
.checked_add(desired_precision)
.ok_or(Error::Overflow)?;
let width = desired_width.max(unpadded_width);
// Pad on the left with spaces to the desired width?
if !flags.left_adj && !flags.zero_pad {
@@ -560,7 +602,8 @@ pub fn sprintf_locale(
}
// Pad on the left to the given precision?
pad(f, '0', prec, body_len)?;
// TODO: why pad with 0 here?
pad(f, '0', desired_precision, body_width)?;
// Output the actual value, perhaps with grouping.
if wants_grouping {

View File

@@ -13,6 +13,7 @@ macro_rules! sprintf_check {
$(,)? // optional trailing comma
) => {
{
use unicode_width::UnicodeWidthStr;
let mut target = String::new();
let mut args = [$($arg.to_arg()),*];
let len = $crate::printf_c_locale(
@@ -20,7 +21,7 @@ macro_rules! sprintf_check {
$fmt.as_ref() as &str,
&mut args,
).expect("printf failed");
assert!(len == target.len(), "Wrong length returned: {} vs {}", len, target.len());
assert_eq!(len, target.width(), "Wrong length returned");
target
}
};
@@ -723,6 +724,18 @@ fn test_huge_precision_g() {
sprintf_err!("%.2147483648g", f => Error::Overflow);
}
#[test]
fn test_non_ascii() {
assert_fmt!("%3s", "ö" => " ö");
assert_fmt!("%3s", "🇺🇳" => " 🇺🇳");
assert_fmt!("%.3s", "🇺🇳🇺🇳" => "🇺🇳");
assert_fmt!("%.3s", "a🇺🇳" => "a🇺🇳");
assert_fmt!("%.3s", "aa🇺🇳" => "aa");
assert_fmt!("%3.3s", "aa🇺🇳" => " aa");
assert_fmt!("%.1s", "𒈙a" => "𒈙");
assert_fmt!("%3.3s", "👨‍👨‍👧‍👧" => " 👨‍👨‍👧‍👧");
}
#[test]
fn test_errors() {
use Error::*;

View File

@@ -72,7 +72,7 @@ function __fish_bind_complete
printf '%sshift-\tShift modifier…\n' $prefix
set -l key_names minus comma backspace delete escape \
enter up down left right pageup pagedown home end insert tab \
space f(seq 12)
space menu printscreen f(seq 12)
printf '%s\tNamed key\n' $prefix$key_names
end
end

View File

@@ -49,10 +49,10 @@ function __fish_clj_tools -V bb_helper
bb -e "$bb_helper" tools
end
complete -c clj -s X -x -r -k -a "(__fish_complete_list : __fish_clj_aliases)" -d "Use concatenated aliases to modify classpath or supply exec fn/args"
complete -c clj -s A -x -r -k -a "(__fish_complete_list : __fish_clj_aliases)" -d "Use concatenated aliases to modify classpath"
complete -c clj -s M -x -r -k -a "(__fish_complete_list : __fish_clj_aliases)" -d "Use concatenated aliases to modify classpath or supply main opts"
complete -c clj -s T -x -r -k -a "(__fish_complete_list : __fish_clj_tools)" -d "Invoke tool by name or via aliases ala -X"
complete -c clj -s X -x -r -k -a "(__fish_stripprefix='^-\w*X' __fish_complete_list : __fish_clj_aliases)" -d "Use concatenated aliases to modify classpath or supply exec fn/args"
complete -c clj -s A -x -r -k -a "(__fish_stripprefix='^-\w*A' __fish_complete_list : __fish_clj_aliases)" -d "Use concatenated aliases to modify classpath"
complete -c clj -s M -x -r -k -a "(__fish_stripprefix='^-\w*M' __fish_complete_list : __fish_clj_aliases)" -d "Use concatenated aliases to modify classpath or supply main opts"
complete -c clj -s T -x -r -k -a "(__fish_stripprefix='^-\w*T' __fish_complete_list : __fish_clj_tools)" -d "Invoke tool by name or via aliases ala -X"
complete -c clj -f -o Sdeps -r -d "Deps data to use as the last deps file to be merged"
complete -c clj -f -o Spath -d "Compute classpath and echo to stdout only"

View File

@@ -85,7 +85,7 @@ complete -c equery -n '__fish_seen_subcommand_from f files' -s s -l timestamp -d
complete -c equery -n '__fish_seen_subcommand_from f files' -s t -l type -d "Include file type in output"
complete -c equery -n '__fish_seen_subcommand_from f files' -l tree -d "Display results in a tree"
complete -c equery -n '__fish_seen_subcommand_from f files' -s f -l filter -d "Filter output by file type" \
-xa "(__fish_complete_list , __fish_equery_files_filter_args)"
-xa "(__fish_stripprefix='^(--filter=|-\w*f)' __fish_complete_list , __fish_equery_files_filter_args)"
# has + hasuse
complete -c equery -n '__fish_seen_subcommand_from a has h hasuse' -s I -l exclude-installed -d "Exclude installed pkgs from search path"

View File

@@ -653,6 +653,7 @@ function __fish_git_aliased_command
end
end
set -g __fish_git_aliases
git config -z --get-regexp 'alias\..*' | while read -lz alias cmdline
set -l command (__fish_git_aliased_command $cmdline)
string match -q --regex '\w+' -- $command; or continue
@@ -778,7 +779,8 @@ function __fish_git_custom_commands
# if any of these completion results match the name of the builtin git commands,
# but it's simpler just to blacklist these names. They're unlikely to change,
# and the failure mode is we accidentally complete a plumbing command.
for name in (string replace -r "^.*/git-([^/]*)" '$1' $PATH/git-*)
set -l git_subcommands $PATH/git-*
for name in (string replace -r "^.*/git-([^/]*)" '$1' $git_subcommands)
switch $name
case cvsserver receive-pack shell upload-archive upload-pack
# skip these
@@ -2593,7 +2595,8 @@ end
# source git-* commands' autocompletion file if exists
set -l __fish_git_custom_commands_completion
for file in (path filter -xZ $PATH/git-* | path basename)
set -l git_subcommands $PATH/git-*
for file in (path filter -xZ $git_subcommands | path basename)
# Already seen this command earlier in $PATH.
contains -- $file $__fish_git_custom_commands_completion
and continue

View File

@@ -4,5 +4,5 @@ complete -c gpasswd -s d -l delete -d 'Remove user from group' -xa '(__fish_comp
complete -c gpasswd -s h -l help -d 'Print help'
complete -c gpasswd -s r -l remove-password -d 'Remove the GROUP\'s password'
complete -c gpasswd -s R -l restrict -d 'Restrict access to GROUP to its members'
complete -c gpasswd -s M -l members -d 'Set the list of members of GROUP' -xa '(__fish_complete_list , __fish_complete_users)'
complete -c gpasswd -s A -l administrators -d 'set the list of administrators for GROUP' -xa '(__fish_complete_list , __fish_complete_users)'
complete -c gpasswd -s M -l members -d 'Set the list of members of GROUP' -xa "(__fish_stripprefix='^(--members=|-\w*M)' __fish_complete_list , __fish_complete_users)"
complete -c gpasswd -s A -l administrators -d 'set the list of administrators for GROUP' -xa "(__fish_stripprefix='^(--administrators=|-\w*A)' __fish_complete_list , __fish_complete_users)"

View File

@@ -36,7 +36,7 @@ complete -c $command -s x -x \
-n $compile_condition
complete -c $command -s W -l warning \
-a '(__fish_complete_list , __fish_guild__complete_warnings)' \
-a "(__fish_stripprefix='^(--warning=|-\w*W)' __fish_complete_list , __fish_guild__complete_warnings)" \
-d 'Specify the warning level for a compilation' \
-n $compile_condition

View File

@@ -85,7 +85,7 @@ complete -c $command -o ds \
-d 'Treat the last -s option as if it occurred at this point'
complete -c $command -l use-srfi \
-a '(__fish_complete_list , __fish_guile__complete_srfis)' \
-a "(__fish_stripprefix='^--use-srfi=' __fish_complete_list , __fish_guile__complete_srfis)" \
-d 'Specify the SRFI modules to load'
for standard in 6 7

View File

@@ -58,7 +58,7 @@ complete -c hashcat -l restore -d "Restore session from --session"
complete -c hashcat -l restore-disable -d "Do not write restore file"
complete -c hashcat -l restore-file-path -rF -d "Specific path to restore file"
complete -c hashcat -s o -l outfile -rF -d "Define outfile for recovered hash"
complete -c hashcat -l outfile-format -xa "(__fish_complete_list , __fish_hashcat_outfile_formats)" -d "Outfile formats to use"
complete -c hashcat -l outfile-format -xa "(__fish_stripprefix='^--outfile-format=' __fish_complete_list , __fish_hashcat_outfile_formats)" -d "Outfile formats to use"
complete -c hashcat -l outfile-autohex-disable -d "Disable the use of \$HEX[] in output plains"
complete -c hashcat -l outfile-check-timer -x -d "Sets seconds between outfile checks"
complete -c hashcat -l wordlist-autohex-disable -d "Disable the conversion of \$HEX[] from the wordlist"
@@ -106,7 +106,7 @@ complete -c hashcat -l backend-ignore-metal -d "Do not try to open Metal interfa
complete -c hashcat -l backend-ignore-opencl -d "Do not try to open OpenCL interface on startup"
complete -c hashcat -s I -l backend-info -d "Show info about detected backend API devices"
complete -c hashcat -s d -l backend-devices -x -d "Backend devices to use"
complete -c hashcat -s D -l opencl-device-types -xa "(__fish_complete_list , __fish_hashcat_device_types)" -d "OpenCL device-types to use"
complete -c hashcat -s D -l opencl-device-types -xa "(__fish_stripprefix='^(--opencl-device-types=|-\w*D)' __fish_complete_list , __fish_hashcat_device_types)" -d "OpenCL device-types to use"
complete -c hashcat -s O -l optimized-kernel-enable -d "Enable optimized kernels (limits password length)"
complete -c hashcat -s M -l multiply-accel-disable -d "Disable multiply kernel-accel with processor count"
complete -c hashcat -s w -l workload-profile -d "Enable a specific workload profile" -xa "

View File

@@ -9,7 +9,7 @@ function __fish_john_formats --description "Print JohnTheRipper hash formats"
end
complete -c john -l help -d "print usage summary"
complete -c john -l single -fa "(__fish_complete_list , __fish_john_rules)" -d "single crack mode"
complete -c john -l single -fa "(__fish_stripprefix='^--single=' __fish_complete_list , __fish_john_rules)" -d "single crack mode"
complete -c john -l single-seed -rf -d "add static seed word(s) for all salts in single mode"
complete -c john -l single-wordlist -rF -d "short wordlist with static seed words/morphemes"
complete -c john -l single-user-seed -rF -d "wordlist with seeds per username"
@@ -35,8 +35,8 @@ complete -c john -l prince-case-permute -d "permute case of first letter"
complete -c john -l prince-mmap -d "memory-map infile"
complete -c john -l prince-keyspace -d "just show total keyspace that would be produced"
complete -c john -l encoding -l input-encoding -fa "$__fish_john_encodings" -d "input encoding"
complete -c john -l rules -fa "(__fish_complete_list , __fish_john_rules)" -d "enable word mangling rules"
complete -c john -l rules-stack -fa "(__fish_complete_list , __fish_john_rules)" -d "stacked rules"
complete -c john -l rules -fa "(__fish_stripprefix='^--rules=' __fish_complete_list , __fish_john_rules)" -d "enable word mangling rules"
complete -c john -l rules-stack -fa "(__fish_stripprefix='^--rules-stack=' __fish_complete_list , __fish_john_rules)" -d "stacked rules"
complete -c john -l rules-skip-nop -d "skip any NOP rules"
complete -c john -l incremental -fa "(john --list=inc-modes 2>/dev/null)" -d "incremental mode"
complete -c john -l incremental-charcount -rf -d "override CharCount for incremental mode"
@@ -97,4 +97,4 @@ complete -c john -l internal-codepage -fa "$__fish_john_encodings" -d "codepage
complete -c john -l target-encoding -fa "$__fish_john_encodings" -d "output encoding"
complete -c john -l tune -fa "auto report N" -d "tuning options"
complete -c john -l force-tty -d "set up terminal for reading keystrokes"
complete -c john -l format -fa "(__fish_complete_list , __fish_john_formats)" -d "force hash type"
complete -c john -l format -fa "(__fish_stripprefix='^--format=' __fish_complete_list , __fish_john_formats)" -d "force hash type"

View File

@@ -41,7 +41,7 @@ complete -c losetup -s v -l verbose -d "Verbose mode"
complete -c losetup -s J -l json -d "Use JSON --list output format"
complete -c losetup -s l -l list -d "List info about all or specified"
complete -c losetup -s n -l noheadings -d "Don't print headings for --list output"
complete -c losetup -s O -l output -x -a "(__fish_complete_list , __fish_print_losetup_list_output)" -d "Specify columns to output for --list"
complete -c losetup -s O -l output -x -a "(__fish_stripprefix='^(--output=|-\w*O)' __fish_complete_list , __fish_print_losetup_list_output)" -d "Specify columns to output for --list"
complete -c losetup -l output-all -d "Output all columns"
complete -c losetup -l raw -d "Use raw --list output format"
complete -c losetup -s h -l help -d "Display help"

View File

@@ -25,7 +25,7 @@ complete -c lpadmin -s o -xa printer-is-shared=true -d 'Sets dest to shared/publ
complete -c lpadmin -s o -xa printer-is-shared=false -d 'Sets dest to shared/published or unshared/unpublished'
complete -c lpadmin -s o -d 'Set IPP operation policy associated with dest' -xa "printer-policy=(test -r /etc/cups/cupsd.conf; and string replace -r --filter '<Policy (.*)>' '$1' < /etc/cups/cupsd.conf)"
complete -c lpadmin -s u -xa 'allow:all allow:none (__fish_complete_list , __fish_complete_users allow:)' -d 'Sets user-level access control on a destination'
complete -c lpadmin -s u -xa '(__fish_complete_list , __fish_complete_groups allow: @)' -d 'Sets user-level access control on a destination'
complete -c lpadmin -s u -xa 'deny:all deny:none (__fish_complete_list , __fish_complete_users deny:)' -d 'Sets user-level access control on a destination'
complete -c lpadmin -s u -xa '(__fish_complete_list , __fish_complete_groups deny: @)' -d 'Sets user-level access control on a destination'
complete -c lpadmin -s u -xa "allow:all allow:none (__fish_stripprefix='^-\w*u' __fish_complete_list , __fish_complete_users allow:)" -d 'Sets user-level access control on a destination'
complete -c lpadmin -s u -xa "(__fish_stripprefix='^-\w*u' __fish_complete_list , __fish_complete_groups allow: @)" -d 'Sets user-level access control on a destination'
complete -c lpadmin -s u -xa "deny:all deny:none (__fish_stripprefix='^-\w*u' __fish_complete_list , __fish_complete_users deny:)" -d 'Sets user-level access control on a destination'
complete -c lpadmin -s u -xa "(__fish_stripprefix='^-\w*u' __fish_complete_list , __fish_complete_groups deny: @)" -d 'Sets user-level access control on a destination'

View File

@@ -12,7 +12,7 @@ complete -c lsblk -s h -l help -d "usage information (this)"
complete -c lsblk -s i -l ascii -d "use ascii characters only"
complete -c lsblk -s m -l perms -d "output info about permissions"
complete -c lsblk -s n -l noheadings -d "don't print headings"
complete -c lsblk -s o -l output -d "output columns" -xa '( __fish_complete_list , __fish_print_lsblk_columns )'
complete -c lsblk -s o -l output -d "output columns" -xa "(__fish_stripprefix='^(--output=|-\w*o)' __fish_complete_list , __fish_print_lsblk_columns)"
complete -c lsblk -s P -l pairs -d "use key='value' output format"
complete -c lsblk -s r -l raw -d "use raw output format"
complete -c lsblk -s t -l topology -d "output info about topology"

View File

@@ -11,9 +11,9 @@ i\t"ignore the device cache file"
r\t"read the device cache file"
u\t"read and update the device cache file"'
complete -c lsof -s g -d 'select by group (^ - negates)' -xa '(__fish_complete_list , __fish_complete_groups)'
complete -c lsof -s g -d 'select by group (^ - negates)' -xa "(__fish_stripprefix='^-\w*g' __fish_complete_list , __fish_complete_groups)"
complete -c lsof -s l -d 'Convert UIDs to login names'
complete -c lsof -s p -d 'Select or exclude processes by pid' -xa '(__fish_complete_list , __fish_complete_pids)'
complete -c lsof -s p -d 'Select or exclude processes by pid' -xa "(__fish_stripprefix='^-\w*p' __fish_complete_list , __fish_complete_pids)"
complete -c lsof -s R -d 'Print PPID'
complete -c lsof -s t -d 'Produce terse output (pids only, no header)'
complete -c lsof -s u -d 'select by user (^ - negates)' -xa '(__fish_complete_list , __fish_complete_users)'
complete -c lsof -s u -d 'select by user (^ - negates)' -xa "(__fish_stripprefix='^-\w*u' __fish_complete_list , __fish_complete_users)"

View File

@@ -35,7 +35,7 @@ function __fish_complete_openssl_ciphers
printf "%s\tCipher String\n" $cs
end
end
complete -c ncat -l ssl-ciphers -x -a "(__fish_complete_list : __fish_complete_openssl_ciphers)" -d "Specify SSL ciphersuites"
complete -c ncat -l ssl-ciphers -x -a "(__fish_stripprefix='^--ssl-ciphers=' __fish_complete_list : __fish_complete_openssl_ciphers)" -d "Specify SSL ciphersuites"
complete -c ncat -l ssl-servername -x -a "(__fish_print_hostnames)" -d "Request distinct server name"
complete -c ncat -l ssl-alpn -x -d "Specify ALPN protocol list"

View File

@@ -92,11 +92,11 @@ function __fish_complete_nmap_script
end
echo -e $__fish_nmap_script_completion_cache
end
complete -c nmap -l script -r -a "(__fish_complete_list , __fish_complete_nmap_script)"
complete -c nmap -l script -r -a "(__fish_stripprefix='^--script=' __fish_complete_list , __fish_complete_nmap_script)"
complete -c nmap -l script -r -d 'Runs a script scan'
complete -c nmap -l script-args -d 'provide arguments to NSE scripts'
complete -c nmap -l script-args-file -r -d 'load arguments to NSE scripts from a file'
complete -c nmap -l script-help -r -a "(__fish_complete_list , __fish_complete_nmap_script)"
complete -c nmap -l script-help -r -a "(__fish_stripprefix='^--script-help=' __fish_complete_list , __fish_complete_nmap_script)"
complete -c nmap -l script-help -r -d "Shows help about scripts"
complete -c nmap -l script-trace
complete -c nmap -l script-updatedb

View File

@@ -1,10 +1,9 @@
set -l nmoutput (nmcli -g NAME connection show --active 2>/dev/null)
or exit # networkmanager isn't running, no point in completing
set -l cname (string escape -- $nmoutput\t"Active connection")
set -a cname (string escape -- (nmcli -g NAME connection show)\t"Connection")
set -l ifname (string escape -- (nmcli -g DEVICE device status)\t"Interface name")
set -l ssid (string escape -- (nmcli -g SSID device wifi list)\t"SSID")
set -l bssid (string escape -- (nmcli -g BSSID device wifi list | string replace --all \\ '')\t"BSSID")
set -l nmoutput '(nmcli -g NAME connection show --active 2>/dev/null)'
set -l cname "$nmoutput"\t"Active connection"
set -a cname '(nmcli -g NAME connection show 2>/dev/null)\t"Connection"'
set -l ifname '(nmcli -g DEVICE device status 2>/dev/null)\t"Interface name"'
set -l ssid '(nmcli -g SSID device wifi list 2>/dev/null)\t"SSID"'
set -l bssid '(nmcli -g BSSID device wifi list 2>/dev/null | string replace --all \\\ "")\t"BSSID"'
set -l nmcli_commands general networking radio connection device agent monitor help
set -l nmcli_general status hostname permissions logging help

View File

@@ -10,7 +10,7 @@ if test "$gnu_linux" -eq 1
# Some short options are GNU-only
complete -c ps -s a -d "Select all processes except session leaders and terminal-less"
complete -c ps -s A -d "Select all"
complete -c ps -s C -d "Select by command" -ra '(__fish_complete_list , __fish_complete_proc)'
complete -c ps -s C -d "Select by command" -ra "(__fish_stripprefix='^-\w*C' __fish_complete_list , __fish_complete_proc)"
complete -c ps -s c -d 'Show different scheduler information for the -l option'
complete -c ps -s d -d "Select all processes except session leaders"
complete -c ps -s e -d "Select all"
@@ -24,9 +24,9 @@ if test "$gnu_linux" -eq 1
complete -c ps -s m -d 'Show threads after processes'
complete -c ps -s N -d "Invert selection"
complete -c ps -s n -d "Set namelist file" -r
complete -c ps -s s -l sid -d "Select by session ID" -x -a "(__fish_complete_list , __fish_complete_pids)"
complete -c ps -s s -l sid -d "Select by session ID" -x -a "(__fish_stripprefix='^(--sid=|-\w*s)' __fish_complete_list , __fish_complete_pids)"
complete -c ps -s T -d "Show threads. With SPID"
complete -c ps -s u -l user -d "Select by user" -x -a "(__fish_complete_list , __fish_complete_users)"
complete -c ps -s u -l user -d "Select by user" -x -a "(__fish_stripprefix='^(--script=|-\w*u)' __fish_complete_list , __fish_complete_users)"
complete -c ps -s V -l version -d "Display version and exit"
complete -c ps -s y -d "Do not show flags"
@@ -39,7 +39,7 @@ if test "$gnu_linux" -eq 1
complete -c ps -l info -d "Display debug info"
complete -c ps -l lines -l rows -d "Set screen height" -r
complete -c ps -l no-headers -d 'Print no headers'
complete -c ps -l ppid -d "Select by parent PID" -x -a "(__fish_complete_list , __fish_complete_pids)"
complete -c ps -l ppid -d "Select by parent PID" -x -a "(__fish_stripprefix='^--ppid=' __fish_complete_list , __fish_complete_pids)"
complete -c ps -l sort -d 'Specify sort order' -r
else
# Assume BSD options otherwise
@@ -81,6 +81,6 @@ end
complete -c ps -s o -lformat$bsd_null -d "User defined format" -x
complete -c ps -s Z -lcontext$bsd_null -d "Include security info"
complete -c ps -s t -ltty$bsd_null -d "Select by tty" -r
complete -c ps -s G -lgroup$bsd_null -d "Select by group" -x -a "(__fish_complete_list , __fish_complete_groups)"
complete -c ps -s U -luser$bsd_null -d "Select by user" -x -a "(__fish_complete_list , __fish_complete_users)"
complete -c ps -s p -lpid$bsd_null -d "Select by PID" -x -a "(__fish_complete_list , __fish_complete_pids)"
complete -c ps -s G -lgroup$bsd_null -d "Select by group" -x -a "(__fish_stripprefix='^(--group=|-\w*G)' __fish_complete_list , __fish_complete_groups)"
complete -c ps -s U -luser$bsd_null -d "Select by user" -x -a "(__fish_stripprefix='^(--user=|-\w*U)' __fish_complete_list , __fish_complete_users)"
complete -c ps -s p -lpid$bsd_null -d "Select by PID" -x -a "(__fish_stripprefix='^(--pid=|-\w*p)' __fish_complete_list , __fish_complete_pids)"

View File

@@ -15,7 +15,7 @@ complete -c setxkbmap -o keycodes -d 'Specifies keycodes component name' -xa "(s
complete -c setxkbmap -o keymap -d 'Specifies name of keymap to load' -xa "(sed -r $filter /usr/share/X11/xkb/keymap.dir)"
complete -c setxkbmap -o layout -d 'Specifies layout used to choose component names' -xa "(__fish_complete_setxkbmap layout)"
complete -c setxkbmap -o model -d 'Specifies model used to choose component names' -xa "(__fish_complete_setxkbmap model)"
complete -c setxkbmap -o option -d 'Adds an option used to choose component names' -xa "(__fish_complete_list , '__fish_complete_setxkbmap option')"
complete -c setxkbmap -o option -d 'Adds an option used to choose component names' -xa "(__fish_stripprefix='^--option=' __fish_complete_list , '__fish_complete_setxkbmap option')"
complete -c setxkbmap -o print -d 'Print a complete xkb_keymap description and exit'
complete -c setxkbmap -o query -d 'Print the current layout settings and exit'
complete -c setxkbmap -o rules -d 'Name of rules file to use' -x

View File

@@ -25,7 +25,7 @@ complete -c ssh -s k -d "Disables forwarding of GSSAPI credentials"
complete -c ssh -s L -d "Specify local port forwarding" -x
complete -c ssh -s l -x -a "(__fish_complete_users)" -d User
complete -c ssh -s M -d "Places the ssh client into master mode"
complete -c ssh -s m -d "MAC algorithm" -xa "(__fish_complete_list , __fish_ssh_macs)"
complete -c ssh -s m -d "MAC algorithm" -xa "(__fish_stripprefix='^-\w*m' __fish_complete_list , __fish_ssh_macs)"
complete -c ssh -s N -d "Do not execute remote command"
complete -c ssh -s n -d "Prevent reading from stdin"
complete -c ssh -s O -d "Control an active connection multiplexing master process" -x

View File

@@ -12,6 +12,6 @@ complete -c su -s G -l supp-group -x -a "(__fish_complete_groups)" -d "Specify a
complete -c su -s m -s p -l preserve_environment -d "Preserve environment"
complete -c su -s P -l pty -d "Create pseudo-terminal for the session"
complete -c su -s s -l shell -x -a "(cat /etc/shells)" -d "Run the specified shell"
complete -c su -s w -l whitelist-environment -x -a "(__fish_complete_list , __fish_complete_su_env_whitelist)" -d "Don't reset these environment variables"
complete -c su -s w -l whitelist-environment -x -a "(__fish_stripprefix='^(--whitelist-environment=|-\w*w)' __fish_complete_list , __fish_complete_su_env_whitelist)" -d "Don't reset these environment variables"
complete -c su -s h -l help -d "Display help and exit"
complete -c su -s V -l version -d "Display version and exit"

View File

@@ -40,6 +40,6 @@ complete -c systemd-cryptenroll -l fido2-with-user-presence -xa "yes no" -d "Req
complete -c systemd-cryptenroll -l fido2-with-user-verification -xa "yes no" -d "Require user verification when unlocking the volume"
complete -c systemd-cryptenroll -l tpm2-device -kxa "(__fish_cryptenroll_tpm2_devices)" -d "Enroll a TPM2 security chip"
complete -c systemd-cryptenroll -l tpm2-pcrs -x -d "Bind the enrollment of TPM2 device to speficied PCRs"
complete -c systemd-cryptenroll -l wipe-slot -kxa "(__fish_complete_list , __fish_cryptenroll_complete_wipe)" -d "Wipes one or more LUKS2 key slots"
complete -c systemd-cryptenroll -l wipe-slot -kxa "(__fish_stripprefix='^--wipe-slot=' __fish_complete_list , __fish_cryptenroll_complete_wipe)" -d "Wipes one or more LUKS2 key slots"
complete -c systemd-cryptenroll -l help -s h -d "Print a short help"
complete -c systemd-cryptenroll -l version -d "Print a short version string"

View File

@@ -5,7 +5,7 @@ complete -c usermod -s d -l home -d "Change user's login directory" -r
complete -c usermod -s e -l expiredate -d "Date (YYYY-MM-DD) on which the user account will be disabled" -x
complete -c usermod -s f -l inactive -d "Number of days after a password expires until the account is locked" -xa "(seq 0 365)"
complete -c usermod -s g -l gid -d "Group name or number of the user's new initial login group" -xa "(__fish_complete_groups)"
complete -c usermod -s G -l groups -d "List of groups which the user is also a member of" -xa "(__fish_complete_list , __fish_complete_groups)"
complete -c usermod -s G -l groups -d "List of groups which the user is also a member of" -xa "(__fish_stripprefix='^(--groups=|-\w*G)' __fish_complete_list , __fish_complete_groups)"
complete -c usermod -s l -l login -d "Change user's name" -x
complete -c usermod -s L -l lock -d "Lock user's password" -f
complete -c usermod -s m -l move-home -d "Move the content of the user's home directory to the new location" -f

View File

@@ -50,7 +50,7 @@ complete -c $progname -s d -d 'Enable extra debugging shown to stderr'
complete -c $progname -s h -d 'Show the help message'
complete -c $progname -s i -d 'Ignore repositories defined in configuration files'
complete -c $progname -s M -d 'For remote repositories, the data is fetched and stored in memory only'
complete -c $progname -s p -d 'Match one or more package properties' -xa "(__fish_complete_list , __fish_print_xbps_pkg_props)"
complete -c $progname -s p -d 'Match one or more package properties' -xa "(__fish_stripprefix='^-\w*p' __fish_complete_list , __fish_print_xbps_pkg_props)"
complete -c $progname -s R -d 'Enable repository mode'
complete -c $progname -l repository -d 'Append the specified repository to the top of the list'
complete -c $progname -l regex -d 'Use Extended Regular Expressions'

View File

@@ -14,15 +14,17 @@ where:
set -q prefix[1]
or set -l prefix ""
set -l pat "$(commandline -t)"
#set -l pat $argv[5]
if set -q __fish_stripprefix[1]
set pat "$(string replace -r -- "$__fish_stripprefix" "" $pat)"
end
switch $pat
case "*$div*"
for i in (echo $pat | sed "s/^\(.\+$div\)$iprefix.*\$/\1/")$iprefix(eval $cmd)
string unescape -- $i
for i in (string unescape -- $pat | sed "s/^\(.\+$div\)$iprefix.*\$/\1/")$iprefix(eval $cmd)
printf %s\n $i
end
case '*'
for i in $prefix$iprefix(eval $cmd)
string unescape -- $i
printf %s\n $i
end
end

View File

@@ -1,15 +1,15 @@
function __fish_complete_pgrep -d 'Complete pgrep/pkill' --argument-names cmd
complete -c $cmd -xa '(__fish_complete_proc)'
complete -c $cmd -s f -d 'Match pattern against full command line'
complete -c $cmd -s g -d 'Only match processes in the process group' -xa '(__fish_complete_list , __fish_complete_groups)'
complete -c $cmd -s G -d "Only match processes whose real group ID is listed. Group 0 is translated into $cmd\'s own process group" -xa '(__fish_complete_list , __fish_complete_groups)'
complete -c $cmd -s g -d 'Only match processes in the process group' -xa "(__fish_stripprefix='^-\w*g' __fish_complete_list , __fish_complete_groups)"
complete -c $cmd -s G -d "Only match processes whose real group ID is listed. Group 0 is translated into $cmd\'s own process group" -xa "(__fish_stripprefix='^-\w*G' __fish_complete_list , __fish_complete_groups)"
complete -c $cmd -s n -d 'Select only the newest process'
complete -c $cmd -s o -d 'Select only the oldest process'
complete -c $cmd -s P -d 'Only match processes whose parent process ID is listed' -xa '(__fish_complete_list , __fish_complete_pids)'
complete -c $cmd -s P -d 'Only match processes whose parent process ID is listed' -xa "(__fish_stripprefix='^-\w*P' __fish_complete_list , __fish_complete_pids)"
complete -c $cmd -s s -d "Only match processes whose process session ID is listed. Session ID 0 is translated into $cmd\'s own session ID."
complete -c $cmd -s t -d 'Only match processes whose controlling terminal is listed. The terminal name should be specified without the "/dev/" prefix' -r
complete -c $cmd -s u -d 'Only match processes whose effective user ID is listed' -xa '(__fish_complete_list , __fish_complete_users)'
complete -c $cmd -s U -d 'Only match processes whose real user ID is listed' -xa '(__fish_complete_list , __fish_complete_users)'
complete -c $cmd -s u -d 'Only match processes whose effective user ID is listed' -xa "(__fish_stripprefix='^-\w*u' __fish_complete_list , __fish_complete_users)"
complete -c $cmd -s U -d 'Only match processes whose real user ID is listed' -xa "(__fish_stripprefix='^-\w*U' __fish_complete_list , __fish_complete_users)"
complete -c $cmd -s v -d 'Negates the matching'
complete -c $cmd -s x -d ' Only match processes whose name (or command line if -f is specified) exactly match the pattern'
end

View File

@@ -3,7 +3,7 @@ function __fish_complete_ssh -d "common completions for ssh commands" --argument
complete -c $command -s 6 -d "IPv6 only"
complete -c $command -s A -d "Enables forwarding of the authentication agent"
complete -c $command -s C -d "Compress all data"
complete -c $command -s c -d "Encryption algorithm" -xa "(__fish_complete_list , __fish_ssh_ciphers)"
complete -c $command -s c -d "Encryption algorithm" -xa "(__fish_stripprefix='^-\w*c' __fish_complete_list , __fish_ssh_ciphers)"
complete -c $command -s F -d "Configuration file" -rF
complete -c $command -s i -d "Identity key file" -rF
complete -c $command -s J -d 'ProxyJump host' -xa "(__fish_complete_user_at_hosts)"

View File

@@ -219,12 +219,17 @@ end" >$__fish_config_dir/config.fish
end
# Notify terminals when $PWD changes via OSC 7 (issue #906).
function __fish_update_cwd_osc --on-variable PWD --description 'Notify terminals when $PWD changes'
set -l host $hostname
if set -q KONSOLE_VERSION
set host ''
if not functions --query __fish_update_cwd_osc
function __fish_update_cwd_osc --on-variable PWD --description 'Notify terminals when $PWD changes'
set -l host $hostname
if set -q KONSOLE_VERSION
set host ''
end
if [ "$TERM" = dumb ]
return
end
printf \e\]7\;file://%s%s\a $host (string escape --style=url -- $PWD)
end
printf \e\]7\;file://%s%s\a $host (string escape --style=url -- $PWD)
end
__fish_update_cwd_osc # Run once because we might have already inherited a PWD from an old tab

View File

@@ -6,5 +6,5 @@
function __fish_seen_subcommand_from
set -l regex (string escape --style=regex -- (commandline -pxc)[2..] | string join '|')
string match -rq -- "^$regex"'$' $argv
string match -rq -- "^($regex)\$" $argv
end

View File

@@ -59,6 +59,9 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod
$legacy_bind --preset $argv \e\[1\;9C nextd-or-forward-word # iTerm2 < 3.5.12
$legacy_bind --preset $argv \e\[1\;9D prevd-or-backward-word # iTerm2 < 3.5.12
bind --preset $argv alt-b prevd-or-backward-word
bind --preset $argv alt-f nextd-or-forward-word
bind --preset $argv alt-up history-token-search-backward
bind --preset $argv alt-down history-token-search-forward
$legacy_bind --preset $argv \e\[1\;9A history-token-search-backward # iTerm2 < 3.5.12

View File

@@ -51,17 +51,17 @@ function fish_default_key_bindings -d "emacs-like key binds"
bind --preset $argv ctrl-/ undo
bind --preset $argv ctrl-_ undo # XTerm idiosyncracy, can get rid of this once we go full CSI u
bind --preset $argv ctrl-z undo
bind --preset $argv ctrl-Z redo
bind --preset $argv ctrl-shift-z redo
bind --preset $argv alt-/ redo
bind --preset $argv alt-t transpose-words
bind --preset $argv alt-u upcase-word
bind --preset $argv alt-c capitalize-word
bind --preset $argv alt-backspace backward-kill-word
bind --preset $argv alt-delete kill-word
bind --preset $argv ctrl-alt-h backward-kill-word
bind --preset $argv ctrl-backspace backward-kill-word
bind --preset $argv ctrl-delete kill-word
bind --preset $argv alt-b prevd-or-backward-word
bind --preset $argv alt-f nextd-or-forward-word
bind --preset $argv alt-\< beginning-of-buffer
bind --preset $argv alt-\> end-of-buffer

View File

@@ -247,7 +247,7 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
# in vim p means paste *after* current character, so go forward a char before pasting
# also in vim, P means paste *at* current position (like at '|' with cursor = line),
# \ so there's no need to go back a char, just paste it without moving
bind -s --preset p 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_modefish_cursor_end_modeinclusive' yank
bind -s --preset p 'set -g fish_cursor_end_mode exclusive' forward-char 'set -g fish_cursor_end_mode inclusive' yank
bind -s --preset P yank
bind -s --preset g,p yank-pop
@@ -261,10 +261,10 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
# Lowercase r, enters replace_one mode
#
bind -s --preset -m replace_one r repaint-mode
bind -s --preset -M replace_one -m default '' delete-char self-insert backward-char repaint-mode
bind -s --preset -M replace_one -m default enter 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode'
bind -s --preset -M replace_one -m default ctrl-j 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode'
bind -s --preset -M replace_one -m default ctrl-m 'commandline -f delete-char; commandline -i \n; commandline -f backward-char; commandline -f repaint-mode'
bind -s --preset -M replace_one -m default '' 'set -g fish_cursor_end_mode exclusive' delete-char self-insert backward-char repaint-mode 'set -g fish_cursor_end_mode inclusive'
bind -s --preset -M replace_one -m default enter 'set -g fish_cursor_end_mode exclusive' 'commandline -f delete-char; commandline -i \n; commandline -f backward-char' repaint-mode 'set -g fish_cursor_end_mode inclusive'
bind -s --preset -M replace_one -m default ctrl-j 'set -g fish_cursor_end_mode exclusive' 'commandline -f delete-char; commandline -i \n; commandline -f backward-char' repaint-mode 'set -g fish_cursor_end_mode inclusive'
bind -s --preset -M replace_one -m default ctrl-m 'set -g fish_cursor_end_mode exclusive' 'commandline -f delete-char; commandline -i \n; commandline -f backward-char' repaint-mode 'set -g fish_cursor_end_mode inclusive'
bind -s --preset -M replace_one -m default escape cancel repaint-mode
bind -s --preset -M replace_one -m default ctrl-\[ cancel repaint-mode

View File

@@ -656,7 +656,8 @@ fn index_mut(&mut self, index: usize) -> &mut Self::Output {
impl Acceptor for $name {
#[allow(unused_variables)]
fn accept<'a>(&'a self, visitor: &mut dyn NodeVisitor<'a>, reversed: bool) {
accept_list_visitor!(Self, accept, visit, self, visitor, reversed, $contents);
let _ =
accept_list_visitor!(Self, accept, visit, self, visitor, reversed, $contents);
}
}
impl AcceptorMut for $name {
@@ -743,7 +744,7 @@ macro_rules! implement_acceptor_for_branch {
impl Acceptor for $name {
#[allow(unused_variables)]
fn accept<'a>(&'a self, visitor: &mut dyn NodeVisitor<'a>, reversed: bool){
visitor_accept_field!(
let _ = visitor_accept_field!(
Self,
accept,
visit,
@@ -3718,7 +3719,7 @@ fn allocate<T: NodeMut + Default>(&self) -> Box<T> {
// Return the resulting Node pointer. It is never null.
fn allocate_visit<T: NodeMut + Default>(&mut self) -> Box<T> {
let mut result = Box::<T>::default();
self.visit_mut(&mut *result);
let _ = self.visit_mut(&mut *result);
result
}

View File

@@ -21,6 +21,8 @@
#![allow(unstable_name_collisions)]
#![allow(clippy::uninlined_format_args)]
#[cfg(feature = "installable")]
use fish::common::{get_executable_path, wcs2osstring};
#[allow(unused_imports)]
use fish::future::IsSomeAnd;
use fish::{
@@ -29,12 +31,12 @@
BUILTIN_ERR_MISSING, BUILTIN_ERR_UNKNOWN, STATUS_CMD_OK, STATUS_CMD_UNKNOWN,
},
common::{
escape, get_executable_path, save_term_foreground_process_group, scoped_push_replacer,
str2wcstring, wcs2osstring, wcs2string, PACKAGE_NAME, PROFILING_ACTIVE, PROGRAM_NAME,
escape, save_term_foreground_process_group, scoped_push_replacer, str2wcstring, wcs2string,
PACKAGE_NAME, PROFILING_ACTIVE, PROGRAM_NAME,
},
env::{
environment::{env_init, EnvStack, Environment},
ConfigPaths, EnvMode, Statuses,
ConfigPaths, EnvMode, Statuses, CONFIG_PATHS,
},
eprintf,
event::{self, Event},
@@ -65,22 +67,16 @@
use std::fs::File;
use std::mem::MaybeUninit;
use std::os::unix::prelude::*;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::{env, ops::ControlFlow};
const DOC_DIR: &str = env!("DOCDIR");
const DATA_DIR: &str = env!("DATADIR");
const DATA_DIR_SUBDIR: &str = env!("DATADIR_SUBDIR");
const SYSCONF_DIR: &str = env!("SYSCONFDIR");
const BIN_DIR: &str = env!("BINDIR");
#[cfg(feature = "installable")]
// Disable for clippy because otherwise it would require sphinx
#[cfg(not(clippy))]
fn install(confirm: bool, dir: PathBuf) -> bool {
fn install(confirm: bool, dir: &PathBuf) -> bool {
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
@@ -192,7 +188,8 @@ fn install(confirm: bool, dir: PathBuf) -> bool {
}
#[cfg(any(clippy, not(feature = "installable")))]
fn install(_confirm: bool, _dir: PathBuf) -> bool {
#[allow(dead_code)]
fn install(_confirm: bool, _dir: &PathBuf) -> bool {
eprintln!("Fish was built without support for self-installation");
return false;
}
@@ -264,128 +261,6 @@ fn print_rusage_self() {
eprintln!(" signals: {signals}");
}
fn determine_config_directory_paths(argv0: impl AsRef<Path>) -> ConfigPaths {
// PORTING: why is this not just an associated method on ConfigPaths?
let mut paths = ConfigPaths::default();
let mut done = false;
let exec_path = get_executable_path(argv0.as_ref());
if let Ok(exec_path) = exec_path.canonicalize() {
FLOG!(
config,
format!("exec_path: {:?}, argv[0]: {:?}", exec_path, argv0.as_ref())
);
// TODO: we should determine program_name from argv0 somewhere in this file
// Detect if we're running right out of the CMAKE build directory
if exec_path.starts_with(env!("CARGO_MANIFEST_DIR")) {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
FLOG!(
config,
"Running out of target directory, using paths relative to CARGO_MANIFEST_DIR:\n",
manifest_dir.display()
);
done = true;
paths = ConfigPaths {
data: manifest_dir.join("share"),
sysconf: manifest_dir.join("etc"),
doc: manifest_dir.join("user_doc/html"),
bin: Some(exec_path.parent().unwrap().to_owned()),
}
}
if !done {
// The next check is that we are in a relocatable directory tree
if exec_path.ends_with("bin/fish") {
let base_path = exec_path.parent().unwrap().parent().unwrap();
paths = ConfigPaths {
// One obvious path is ~/.local (with fish in ~/.local/bin/).
// If we picked ~/.local/share/fish as our data path,
// we would install there and erase history.
// So let's isolate us a bit more.
#[cfg(feature = "installable")]
data: base_path.join("share/fish/install"),
#[cfg(not(feature = "installable"))]
data: base_path.join("share/fish"),
sysconf: base_path.join("etc/fish"),
doc: base_path.join("share/doc/fish"),
bin: Some(base_path.join("bin")),
}
} else if exec_path.ends_with("fish") {
FLOG!(
config,
"'fish' not in a 'bin/', trying paths relative to source tree"
);
let base_path = exec_path.parent().unwrap();
paths = ConfigPaths {
#[cfg(feature = "installable")]
data: base_path.join("share/install"),
#[cfg(not(feature = "installable"))]
data: base_path.join("share"),
sysconf: base_path.join("etc"),
doc: base_path.join("user_doc/html"),
bin: Some(base_path.to_path_buf()),
}
}
if paths.data.exists() && paths.sysconf.exists() {
// The docs dir may not exist; in that case fall back to the compiled in path.
if !paths.doc.exists() {
paths.doc = PathBuf::from(DOC_DIR);
}
done = true;
}
}
}
if !done {
// Fall back to what got compiled in.
let data = if cfg!(feature = "installable") {
let Some(home) = fish::env::get_home() else {
FLOG!(
error,
"Cannot find home directory and will refuse to read configuration.\n",
"Consider installing into a directory tree with `fish --install=PATH`."
);
return paths;
};
PathBuf::from(home).join(DATA_DIR).join(DATA_DIR_SUBDIR)
} else {
PathBuf::from(DATA_DIR).join(DATA_DIR_SUBDIR)
};
let bin = if cfg!(feature = "installable") {
exec_path.parent().map(|x| x.to_path_buf())
} else {
Some(PathBuf::from(BIN_DIR))
};
FLOG!(config, "Using compiled in paths:");
paths = ConfigPaths {
data,
sysconf: PathBuf::from(SYSCONF_DIR).join("fish"),
doc: DOC_DIR.into(),
bin,
}
}
FLOGF!(
config,
"determine_config_directory_paths() results:\npaths.data: %ls\npaths.sysconf: \
%ls\npaths.doc: %ls\npaths.bin: %ls",
paths.data.display().to_string(),
paths.sysconf.display().to_string(),
paths.doc.display().to_string(),
paths
.bin
.clone()
.map(|x| x.display().to_string())
.unwrap_or("|not found|".to_string()),
);
paths
}
// Source the file config.fish in the given directory.
// Returns true if successful, false if not.
fn source_config_in_directory(parser: &Parser, dir: &wstr) -> bool {
@@ -463,7 +338,7 @@ fn read_init(parser: &Parser, paths: &ConfigPaths) {
);
}
install(true, PathBuf::from(wcs2osstring(&datapath)));
install(true, &PathBuf::from(wcs2osstring(&datapath)));
// We try to go on if installation failed (or was rejected) here
// If the assets are missing, we will trigger a later error,
// if they are outdated, things will probably (tm) work somewhat.
@@ -592,8 +467,9 @@ fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow<i
// path/share/fish/ is the data directory
// path/etc/fish is sysconf????
use std::fs;
use std::path::Path;
let dir = PathBuf::from(wcs2osstring(path));
if install(true, dir.join("share/fish/install")) {
if install(true, &dir.join("share/fish/install")) {
for sub in &["share/fish/install", "etc/fish", "bin"] {
let p = dir.join(sub);
let Ok(_) = fs::create_dir_all(p.clone()) else {
@@ -626,14 +502,12 @@ fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow<i
}
}
} else {
let paths = Some(determine_config_directory_paths(OsString::from_vec(
wcs2string(&args[0]),
)));
let paths = Some(&*CONFIG_PATHS);
let Some(paths) = paths else {
FLOG!(error, "Cannot find config paths");
std::process::exit(1);
};
install(true, paths.data);
install(true, &paths.data);
}
}
'l' => opts.is_login = true,
@@ -817,18 +691,19 @@ fn throwing_main() -> i32 {
save_term_foreground_process_group();
}
let mut paths: Option<ConfigPaths> = None;
let mut paths: Option<&ConfigPaths> = None;
// If we're not executing, there's no need to find the config.
if !opts.no_exec {
paths = Some(determine_config_directory_paths(OsString::from_vec(
wcs2string(&args[0]),
)));
paths = Some(&*CONFIG_PATHS);
env_init(
paths.as_ref(),
paths,
/* do uvars */ !opts.no_config,
/* default paths */ opts.no_config,
);
}
paths
} else {
None
};
// Set features early in case other initialization depends on them.
// Start with the ones set in the environment, then those set on the command line (so the
@@ -845,7 +720,7 @@ fn throwing_main() -> i32 {
// Construct the root parser!
let env = Rc::new(EnvStack::globals().create_child(true /* dispatches_var_changes */));
let parser: &Parser = &Parser::new(env, CancelBehavior::Clear);
let parser = &Parser::new(env, CancelBehavior::Clear);
parser.set_syncs_uvars(!opts.no_config);
if !opts.no_exec && !opts.no_config {
@@ -1008,7 +883,6 @@ fn fish_xdm_login_hack_hack_hack_hack(cmds: &mut [OsString], args: &[WString]) -
return false;
}
let mut result = false;
let cmd = &cmds[0];
if cmd == "exec \"${@}\"" || cmd == "exec \"$@\"" {
// We're going to construct a new command that starts with exec, and then has the
@@ -1020,7 +894,8 @@ fn fish_xdm_login_hack_hack_hack_hack(cmds: &mut [OsString], args: &[WString]) -
}
cmds[0] = new_cmd;
result = true;
true
} else {
false
}
result
}

View File

@@ -20,10 +20,10 @@
eprintf, fprintf,
input::input_terminfo_get_name,
input_common::{
terminal_protocol_hacks, terminal_protocols_enable_ifn, CharEvent, InputEventQueue,
InputEventQueuer,
match_key_event_to_key, terminal_protocol_hacks, terminal_protocols_enable_ifn, CharEvent,
InputEventQueue, InputEventQueuer, KeyEvent,
},
key::{self, char_to_symbol, Key},
key::{char_to_symbol, Key},
panic::panic_handler,
print_help::print_help,
printf,
@@ -37,15 +37,21 @@
};
/// Return true if the recent sequence of characters indicates the user wants to exit the program.
fn should_exit(recent_keys: &mut Vec<Key>, key: Key) -> bool {
recent_keys.push(key);
fn should_exit(recent_keys: &mut Vec<KeyEvent>, key_evt: KeyEvent) -> bool {
recent_keys.push(key_evt);
for evt in [VINTR, VEOF] {
let modes = shell_modes();
let cc = Key::from_single_byte(modes.c_cc[evt]);
if key == cc {
if recent_keys.iter().rev().nth(1) == Some(&cc) {
if match_key_event_to_key(&key_evt, &cc).is_some() {
if recent_keys
.iter()
.rev()
.nth(1)
.and_then(|&prev| match_key_event_to_key(&prev, &cc))
.is_some()
{
return true;
}
eprintf!(
@@ -101,9 +107,6 @@ fn process_input(continuous_mode: bool, verbose: bool) -> i32 {
continue;
};
let c = kevt.key.codepoint;
if c == key::Invalid {
continue;
}
if verbose {
printf!("# decoded from: ");
for byte in kevt.seq.chars() {
@@ -111,7 +114,27 @@ fn process_input(continuous_mode: bool, verbose: bool) -> i32 {
}
printf!("\n");
}
printf!("bind %s 'do something'\n", kevt.key);
let have_shifted_key = kevt.key.shifted_codepoint != '\0';
let mut keys = vec![(kevt.key.key, "")];
if have_shifted_key {
let mut shifted_key = kevt.key.key;
shifted_key.modifiers.shift = false;
shifted_key.codepoint = kevt.key.shifted_codepoint;
keys.push((shifted_key, "shifted key"));
}
if kevt.key.base_layout_codepoint != '\0' {
let mut base_layout_key = kevt.key.key;
base_layout_key.codepoint = kevt.key.base_layout_codepoint;
keys.push((base_layout_key, "physical key"));
}
for (key, explanation) in keys {
printf!(
"bind %s 'do something'%s%s\n",
key,
if explanation.is_empty() { "" } else { " # " },
explanation,
);
}
if let Some(name) = sequence_name(&mut recent_chars1, c) {
printf!("bind -k %ls 'do something'\n", name);
}

View File

@@ -17,12 +17,13 @@ fn send_to_bg(
let err = {
let job = &jobs[job_pos];
wgettext_fmt!(
"%ls: Can't put job %s, '%ls' to background because it is not under job control\n",
cmd,
job.job_id().to_wstring(),
job.command()
)
"%ls: Can't put job %s, '%ls' to background because it is not under job control\n",
cmd,
job.job_id().to_wstring(),
job.command()
)
};
drop(jobs);
builtin_print_help_error(parser, streams, cmd, &err);
return STATUS_CMD_ERROR;
}

View File

@@ -176,7 +176,7 @@ fn builtin_complete_remove_cmd(
if !removed {
// This means that all loops were empty.
complete_remove_all(cmd.to_owned(), cmd_is_path);
complete_remove_all(cmd.to_owned(), cmd_is_path, /*explicit=*/ true);
}
}

View File

@@ -148,7 +148,7 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Optio
let job_group = job.group();
job_group.set_is_foreground(true);
if job.entitled_to_terminal() {
crate::input_common::terminal_protocols_disable_ifn();
crate::input_common::terminal_protocols_disable_ifn(false);
}
let tmodes = job_group.tmodes.borrow();
if job_group.wants_terminal() && tmodes.is_some() {

View File

@@ -248,7 +248,7 @@ fn read_interactive(
reader_readline(parser, nchars)
};
terminal_protocols_disable_ifn();
terminal_protocols_disable_ifn(false);
if let Some(line) = mline {
*buff = line;
if nchars > 0 && nchars < buff.len() {

View File

@@ -582,11 +582,10 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O
STATUS_CURRENT_CMD => {
let command = &parser.libdata().status_vars.command;
if !command.is_empty() {
streams.out.append(command);
streams.out.appendln(command);
} else {
streams.out.appendln(*PROGRAM_NAME.get().unwrap());
}
streams.out.append_char('\n');
}
STATUS_CURRENT_COMMANDLINE => {
let commandline = &parser.libdata().status_vars.commandline;

View File

@@ -1935,19 +1935,16 @@ fn complete_custom(&mut self, cmd: &wstr, cmdline: &wstr, ad: &mut CustomArgData
// Perhaps set a transient commandline so that custom completions
// builtin_commandline will refer to the wrapped command. But not if
// we're doing autosuggestions.
let mut _remove_transient = None;
let wants_transient =
(ad.wrap_depth > 0 || !ad.var_assignments.is_empty()) && !is_autosuggest;
if wants_transient {
let _remove_transient = (!is_autosuggest).then(|| {
let parser = self.ctx.parser();
parser
.libdata_mut()
.transient_commandlines
.push(cmdline.to_owned());
_remove_transient = Some(ScopeGuard::new((), move |_| {
ScopeGuard::new((), move |_| {
parser.libdata_mut().transient_commandlines.pop();
}));
}
})
});
// Maybe apply variable assignments.
let _restore_vars = self.apply_var_assignments(ad.var_assignments);
@@ -2342,14 +2339,14 @@ pub fn complete_remove(cmd: WString, cmd_is_path: bool, option: &wstr, typ: Comp
}
/// Removes all completions for a given command.
pub fn complete_remove_all(cmd: WString, cmd_is_path: bool) {
pub fn complete_remove_all(cmd: WString, cmd_is_path: bool, explicit: bool) {
let mut completion_map = COMPLETION_MAP.lock().expect("mutex poisoned");
let idx = CompletionEntryIndex {
name: cmd,
is_path: cmd_is_path,
};
let removed = completion_map.remove(&idx).is_some();
if !removed && !idx.is_path {
if explicit && !removed && !idx.is_path {
COMPLETION_TOMBSTONES.lock().unwrap().insert(idx.name);
}
}
@@ -2522,7 +2519,7 @@ pub fn complete_invalidate_path() {
.expect("mutex poisoned")
.get_autoloaded_commands();
for cmd in cmds {
complete_remove_all(cmd, false /* not a path */);
complete_remove_all(cmd, /*cmd_is_path=*/ false, /*explicit=*/ false);
}
}

168
src/env/config_paths.rs vendored Normal file
View File

@@ -0,0 +1,168 @@
use super::ConfigPaths;
use crate::env;
use crate::{common::get_executable_path, FLOG, FLOGF};
use once_cell::sync::Lazy;
use std::path::{Path, PathBuf};
const DOC_DIR: &str = env!("DOCDIR");
const DATA_DIR: &str = env!("DATADIR");
const DATA_DIR_SUBDIR: &str = env!("DATADIR_SUBDIR");
const SYSCONF_DIR: &str = env!("SYSCONFDIR");
const BIN_DIR: &str = env!("BINDIR");
const LOCALE_DIR: &str = env!("LOCALEDIR");
pub static CONFIG_PATHS: Lazy<ConfigPaths> = Lazy::new(|| {
// Read the current executable and follow all symlinks to it.
// OpenBSD has issues with `std::env::current_exe`, see gh-9086 and
// https://github.com/rust-lang/rust/issues/60560
let argv0 = PathBuf::from(std::env::args().next().unwrap());
let argv0 = if argv0.exists() {
argv0
} else {
std::env::current_exe().unwrap_or(argv0)
};
let argv0 = argv0.canonicalize().unwrap_or(argv0);
determine_config_directory_paths(argv0)
});
fn determine_config_directory_paths(argv0: impl AsRef<Path>) -> ConfigPaths {
// PORTING: why is this not just an associated method on ConfigPaths?
let mut paths = ConfigPaths::default();
let mut done = false;
let exec_path = get_executable_path(argv0.as_ref());
if let Ok(exec_path) = exec_path.canonicalize() {
FLOG!(
config,
format!("exec_path: {:?}, argv[0]: {:?}", exec_path, argv0.as_ref())
);
// TODO: we should determine program_name from argv0 somewhere in this file
// Detect if we're running right out of the CMAKE build directory
if exec_path.starts_with(env!("CARGO_MANIFEST_DIR")) {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
FLOG!(
config,
"Running out of target directory, using paths relative to CARGO_MANIFEST_DIR:\n",
manifest_dir.display()
);
done = true;
paths = ConfigPaths {
data: manifest_dir.join("share"),
sysconf: manifest_dir.join("etc"),
doc: manifest_dir.join("user_doc/html"),
bin: Some(exec_path.parent().unwrap().to_owned()),
locale: Some(manifest_dir.join("share/locale")),
}
}
if !done {
// The next check is that we are in a relocatable directory tree
if exec_path.ends_with("bin/fish") {
let base_path = exec_path.parent().unwrap().parent().unwrap();
#[cfg(feature = "installable")]
let data = base_path.join("share/fish/install");
#[cfg(not(feature = "installable"))]
let data = base_path.join("share/fish");
let locale =
(!cfg!(feature = "installable")).then(|| base_path.join("share/locale"));
paths = ConfigPaths {
// One obvious path is ~/.local (with fish in ~/.local/bin/).
// If we picked ~/.local/share/fish as our data path,
// we would install there and erase history.
// So let's isolate us a bit more.
data,
sysconf: base_path.join("etc/fish"),
doc: base_path.join("share/doc/fish"),
bin: Some(base_path.join("bin")),
locale,
}
} else if exec_path.ends_with("fish") {
FLOG!(
config,
"'fish' not in a 'bin/', trying paths relative to source tree"
);
let base_path = exec_path.parent().unwrap();
#[cfg(feature = "installable")]
let data = base_path.join("share/install");
#[cfg(not(feature = "installable"))]
let data = base_path.join("share");
let locale = Some(data.join("locale"));
paths = ConfigPaths {
data,
sysconf: base_path.join("etc"),
doc: base_path.join("user_doc/html"),
bin: Some(base_path.to_path_buf()),
locale,
}
}
if paths.data.exists() && paths.sysconf.exists() {
// The docs dir may not exist; in that case fall back to the compiled in path.
if !paths.doc.exists() {
paths.doc = PathBuf::from(DOC_DIR);
}
done = true;
}
}
}
if !done {
// Fall back to what got compiled in.
let data = if cfg!(feature = "installable") {
let Some(home) = env::get_home() else {
FLOG!(
error,
"Cannot find home directory and will refuse to read configuration.\n",
"Consider installing into a directory tree with `fish --install=PATH`."
);
return paths;
};
PathBuf::from(home).join(DATA_DIR).join(DATA_DIR_SUBDIR)
} else {
Path::new(DATA_DIR).join(DATA_DIR_SUBDIR)
};
let bin = if cfg!(feature = "installable") {
exec_path.parent().map(|x| x.to_path_buf())
} else {
Some(PathBuf::from(BIN_DIR))
};
let locale = if cfg!(feature = "installable") {
None
} else {
Some(PathBuf::from(LOCALE_DIR))
};
FLOG!(config, "Using compiled in paths:");
paths = ConfigPaths {
data,
sysconf: Path::new(SYSCONF_DIR).join("fish"),
doc: DOC_DIR.into(),
bin,
locale,
}
}
FLOGF!(
config,
"determine_config_directory_paths() results:\npaths.data: %ls\npaths.sysconf: \
%ls\npaths.doc: %ls\npaths.bin: %ls\npaths.locale: %ls",
paths.data.display().to_string(),
paths.sysconf.display().to_string(),
paths.doc.display().to_string(),
paths
.bin
.clone()
.map(|x| x.display().to_string())
.unwrap_or("|not found|".to_string()),
paths
.locale
.clone()
.map(|x| x.display().to_string())
.unwrap_or("|not found|".to_string()),
);
paths
}

2
src/env/mod.rs vendored
View File

@@ -1,6 +1,8 @@
mod config_paths;
pub mod environment;
mod environment_impl;
pub mod var;
pub use config_paths::CONFIG_PATHS;
use crate::common::ToCString;
pub use environment::*;

9
src/env/var.rs vendored
View File

@@ -50,10 +50,11 @@ fn from(val: EnvMode) -> Self {
/// env_init.
#[derive(Default)]
pub struct ConfigPaths {
pub data: PathBuf, // e.g., /usr/local/share
pub sysconf: PathBuf, // e.g., /usr/local/etc
pub doc: PathBuf, // e.g., /usr/local/share/doc/fish
pub bin: Option<PathBuf>, // e.g., /usr/local/bin
pub data: PathBuf, // e.g., /usr/local/share
pub sysconf: PathBuf, // e.g., /usr/local/etc
pub doc: PathBuf, // e.g., /usr/local/share/doc/fish
pub bin: Option<PathBuf>, // e.g., /usr/local/bin
pub locale: Option<PathBuf>, // e.g., /usr/local/share/locale
}
/// A collection of status and pipestatus.

View File

@@ -801,7 +801,7 @@ fn save(&mut self, directory: &wstr) -> bool {
{
let mut times: [libc::timespec; 2] = unsafe { std::mem::zeroed() };
times[0].tv_nsec = libc::UTIME_OMIT; // don't change ctime
if unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut times[1]) } != 0 {
if unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut times[1]) } == 0 {
unsafe {
libc::futimens(private_fd.as_raw_fd(), &times[0]);
}

View File

@@ -39,7 +39,7 @@
print_exit_warning_for_jobs, InternalProc, Job, JobGroupRef, Pid, ProcStatus, Process,
ProcessType, TtyTransfer,
};
use crate::reader::{reader_run_count, restore_term_mode};
use crate::reader::{reader_run_count, safe_restore_term_mode};
use crate::redirection::{dup2_list_resolve_chain, Dup2List};
use crate::threads::{iothread_perform_cant_wait, is_forked_child};
use crate::trace::trace_if_enabled_with_args;
@@ -437,7 +437,7 @@ fn launch_process_nofork(vars: &EnvStack, p: &Process) -> ! {
let actual_cmd = wcs2zstring(&p.actual_cmd);
// Ensure the terminal modes are what they were before we changed them.
restore_term_mode(false);
safe_restore_term_mode(false);
// Bounce to launch_process. This never returns.
safe_launch_process(p, &actual_cmd, &argv, &*envp);
}

View File

@@ -120,7 +120,7 @@ pub fn all_categories() -> Vec<&'static category_t> {
(char_encoding, "char-encoding", "Character encoding issues");
(history, "history", "Command history events");
(history_file, "history-file", "Reading/Writing the history file", true);
(history_file, "history-file", "Reading/Writing the history file");
(profile_history, "profile-history", "History performance measurements");

View File

@@ -30,6 +30,9 @@ pub enum FeatureFlag {
/// Whether keyboard protocols (kitty's CSI x u, xterm's modifyOtherKeys) are used
keyboard_protocols,
/// Whether to write OSC 133 prompt markers
mark_prompt,
}
struct Features {
@@ -118,6 +121,14 @@ pub struct FeatureMetadata {
default_value: true,
read_only: false,
},
FeatureMetadata {
flag: FeatureFlag::mark_prompt,
name: L!("mark-prompt"),
groups: L!("4.0"),
description: L!("Write OSC 133 prompt markers to the terminal"),
default_value: true,
read_only: false,
},
];
thread_local!(
@@ -180,6 +191,7 @@ const fn new() -> Self {
AtomicBool::new(METADATA[4].default_value),
AtomicBool::new(METADATA[5].default_value),
AtomicBool::new(METADATA[6].default_value),
AtomicBool::new(METADATA[7].default_value),
],
}
}

View File

@@ -44,7 +44,7 @@
};
use bitflags::bitflags;
use libc::{fchmod, fchown, flock, LOCK_EX, LOCK_SH, LOCK_UN};
use libc::{fchmod, fchown, flock, EINTR, LOCK_EX, LOCK_SH, LOCK_UN};
use lru::LruCache;
use nix::{fcntl::OFlag, sys::stat::Mode};
use rand::Rng;
@@ -1353,8 +1353,15 @@ unsafe fn maybe_lock_file(file: &mut File, lock_type: libc::c_int) -> bool {
return false;
}
let start_time = SystemTime::now();
let retval = unsafe { flock(raw_fd, lock_type) };
let (ok, start_time) = loop {
let start_time = SystemTime::now();
if unsafe { flock(raw_fd, lock_type) } != -1 {
break (true, start_time);
}
if errno::errno().0 != EINTR {
break (false, start_time);
}
};
if let Ok(duration) = start_time.elapsed() {
if duration > Duration::from_millis(250) {
FLOG!(
@@ -1367,7 +1374,7 @@ unsafe fn maybe_lock_file(file: &mut File, lock_type: libc::c_int) -> bool {
ABANDONED_LOCKING.store(true);
}
}
retval != -1
ok
}
/// Unlock a history file.

View File

@@ -3,8 +3,11 @@
use crate::env::{Environment, CURSES_INITIALIZED};
use crate::event;
use crate::flog::FLOG;
#[allow(unused_imports)]
use crate::future::IsSomeAnd;
use crate::input_common::{
CharEvent, CharInputStyle, InputData, InputEventQueuer, ReadlineCmd, R_END_INPUT_FUNCTIONS,
match_key_event_to_key, CharEvent, CharInputStyle, InputData, InputEventQueuer, KeyEvent,
KeyMatchQuality, ReadlineCmd, R_END_INPUT_FUNCTIONS,
};
use crate::key::{self, canonicalize_raw_escapes, ctrl, Key, Modifiers};
use crate::proc::job_reap;
@@ -16,6 +19,7 @@
use crate::wchar::prelude::*;
use once_cell::sync::{Lazy, OnceCell};
use std::ffi::CString;
use std::mem;
use std::sync::{
atomic::{AtomicU32, Ordering},
Mutex, MutexGuard,
@@ -429,7 +433,7 @@ fn select_interrupted(&mut self) {
if reader_reading_interrupted(self) != 0 {
let vintr = shell_modes().c_cc[libc::VINTR];
if vintr != 0 {
self.push_front(CharEvent::from_key(Key::from_single_byte(vintr)));
self.push_front(CharEvent::from_key(KeyEvent::from_single_byte(vintr)));
}
return;
}
@@ -509,14 +513,19 @@ fn next(&mut self) -> CharEvent {
/// Check if the next event is the given character. This advances the index on success only.
/// If `escaped` is set, then return false if this (or any other) character had a timeout.
fn next_is_char(&mut self, style: &KeyNameStyle, key: Key, escaped: bool) -> bool {
fn next_is_char(
&mut self,
style: &KeyNameStyle,
key: Key,
escaped: bool,
) -> Option<KeyMatchQuality> {
assert!(
self.idx <= self.peeked.len(),
"Index must not be larger than dequeued event count"
);
// See if we had a timeout already.
if escaped && self.had_timeout {
return false;
return None;
}
// Grab a new event if we have exhausted what we have already peeked.
// Use either readch or readch_timed, per our param.
@@ -527,7 +536,7 @@ fn next_is_char(&mut self, style: &KeyNameStyle, key: Key, escaped: bool) -> boo
Some(evt) => evt,
None => {
self.had_timeout = true;
return false;
return None;
}
}
} else {
@@ -536,7 +545,7 @@ fn next_is_char(&mut self, style: &KeyNameStyle, key: Key, escaped: bool) -> boo
Some(evt) => evt,
None => {
self.had_timeout = true;
return false;
return None;
}
}
};
@@ -546,9 +555,7 @@ fn next_is_char(&mut self, style: &KeyNameStyle, key: Key, escaped: bool) -> boo
// Now we have peeked far enough; check the event.
// If it matches the char, then increment the index.
let evt = &self.peeked[self.idx];
let Some(kevt) = evt.get_key() else {
return false;
};
let kevt = evt.get_key()?;
if kevt.seq == L!("\x1b") && key.modifiers == Modifiers::ALT {
self.idx += 1;
self.subidx = 0;
@@ -556,13 +563,13 @@ fn next_is_char(&mut self, style: &KeyNameStyle, key: Key, escaped: bool) -> boo
return self.next_is_char(style, Key::from_raw(key.codepoint), true);
}
if *style == KeyNameStyle::Plain {
if kevt.key == key {
let result = match_key_event_to_key(&kevt.key, &key);
if let Some(key_match) = &result {
assert!(self.subidx == 0);
self.idx += 1;
FLOG!(reader, "matched full key", key);
return true;
FLOG!(reader, "matched full key", key, "kind", key_match);
}
return false;
return result;
}
let actual_seq = kevt.seq.as_char_slice();
if !actual_seq.is_empty() {
@@ -582,7 +589,7 @@ fn next_is_char(&mut self, style: &KeyNameStyle, key: Key, escaped: bool) -> boo
actual_seq.len()
)
);
return true;
return Some(KeyMatchQuality::Exact);
}
if key.modifiers == Modifiers::ALT && seq_char == '\x1b' {
if self.subidx + 1 == actual_seq.len() {
@@ -602,11 +609,11 @@ fn next_is_char(&mut self, style: &KeyNameStyle, key: Key, escaped: bool) -> boo
self.subidx = 0;
}
FLOG!(reader, format!("matched {key} against raw escape sequence"));
return true;
return Some(KeyMatchQuality::Exact);
}
}
}
false
None
}
/// Consume all events up to the current index.
@@ -633,7 +640,12 @@ pub fn restart(&mut self) {
}
/// Return true if this `peeker` matches a given sequence of char events given by `str`.
fn try_peek_sequence(&mut self, style: &KeyNameStyle, seq: &[Key]) -> bool {
fn try_peek_sequence(
&mut self,
style: &KeyNameStyle,
seq: &[Key],
quality: &mut Vec<KeyMatchQuality>,
) -> bool {
assert!(
!seq.is_empty(),
"Empty sequence passed to try_peek_sequence"
@@ -643,9 +655,10 @@ fn try_peek_sequence(&mut self, style: &KeyNameStyle, seq: &[Key]) -> bool {
// If we just read an escape, we need to add a timeout for the next char,
// to distinguish between the actual escape key and an "alt"-modifier.
let escaped = *style != KeyNameStyle::Plain && prev == Key::from_raw(key::Escape);
if !self.next_is_char(style, *key, escaped) {
let Some(spec) = self.next_is_char(style, *key, escaped) else {
return false;
}
};
quality.push(spec);
prev = *key;
}
if self.subidx != 0 {
@@ -662,16 +675,24 @@ fn try_peek_sequence(&mut self, style: &KeyNameStyle, seq: &[Key]) -> bool {
/// user's mapping list, then the preset list.
/// Return none if nothing matches, or if we may have matched a longer sequence but it was
/// interrupted by a readline event.
pub fn find_mapping(
pub fn find_mapping<'a>(
&mut self,
vars: &dyn Environment,
ip: &InputMappingSet,
ip: &'a InputMappingSet,
) -> Option<InputMapping> {
let mut generic: Option<&InputMapping> = None;
let bind_mode = input_get_bind_mode(vars);
let mut escape: Option<&InputMapping> = None;
struct MatchedMapping<'a> {
mapping: &'a InputMapping,
quality: Vec<KeyMatchQuality>,
idx: usize,
subidx: usize,
}
let mut deferred: Option<MatchedMapping<'a>> = None;
let ml = ip.mapping_list.iter().chain(ip.preset_mapping_list.iter());
let mut quality = vec![];
for m in ml {
if m.mode != bind_mode {
continue;
@@ -679,24 +700,41 @@ pub fn find_mapping(
// Defer generic mappings until the end.
if m.is_generic() {
if generic.is_none() {
generic = Some(m);
if deferred.is_none() {
deferred = Some(MatchedMapping {
mapping: m,
quality: vec![],
idx: self.idx,
subidx: self.subidx,
});
}
continue;
}
// FLOG!(reader, "trying mapping", format!("{:?}", m));
if self.try_peek_sequence(&m.key_name_style, &m.seq) {
// A binding for just escape should also be deferred
// so escape sequences take precedence.
if m.seq == vec![Key::from_raw(key::Escape)] {
if escape.is_none() {
escape = Some(m);
}
} else {
if self.try_peek_sequence(&m.key_name_style, &m.seq, &mut quality) {
// // A binding for just escape should also be deferred
// // so escape sequences take precedence.
let is_escape = m.seq == vec![Key::from_raw(key::Escape)];
let is_perfect_match = quality
.iter()
.all(|key_match| *key_match == KeyMatchQuality::Exact);
if !is_escape && is_perfect_match {
return Some(m.clone());
}
if deferred
.as_ref()
.is_none_or(|matched| !is_escape && quality >= matched.quality)
{
deferred = Some(MatchedMapping {
mapping: m,
quality: mem::take(&mut quality),
idx: self.idx,
subidx: self.subidx,
});
}
}
quality.clear();
self.restart();
}
if self.char_sequence_interrupted() {
@@ -705,17 +743,13 @@ pub fn find_mapping(
return None;
}
if escape.is_some() {
// We need to reconsume the escape.
self.next();
return escape.cloned();
}
if generic.is_some() {
generic.cloned()
} else {
None
}
deferred
.map(|matched| {
self.idx = matched.idx;
self.subidx = matched.subidx;
matched.mapping
})
.cloned()
}
}
@@ -841,7 +875,7 @@ fn read_characters_no_readline(&mut self) -> CharEvent {
let evt_to_return: CharEvent;
loop {
let evt = self.readch();
if evt.is_readline_or_command() {
if evt.is_readline_or_command() || evt.is_check_exit() {
saved_events.push(evt);
} else {
evt_to_return = evt;

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,9 @@
use crate::{
common::{escape_string, EscapeFlags, EscapeStringStyle},
fallback::fish_wcwidth,
reader::TERMINAL_MODE_ON_STARTUP,
reader::safe_get_terminal_mode_on_startup,
wchar::{decode_byte_from_char, prelude::*},
wutil::{fish_is_pua, fish_wcstoi},
wutil::fish_wcstoul,
};
pub(crate) const Backspace: char = '\u{F500}'; // below ENCODE_DIRECT_BASE
@@ -18,16 +18,19 @@
pub(crate) const Right: char = '\u{F507}';
pub(crate) const PageUp: char = '\u{F508}';
pub(crate) const PageDown: char = '\u{F509}';
pub(crate) const Home: char = '\u{F50a}';
pub(crate) const End: char = '\u{F50b}';
pub(crate) const Insert: char = '\u{F50c}';
pub(crate) const Tab: char = '\u{F50d}';
pub(crate) const Space: char = '\u{F50e}';
pub const Invalid: char = '\u{F50f}';
pub(crate) const Home: char = '\u{F50A}';
pub(crate) const End: char = '\u{F50B}';
pub(crate) const Insert: char = '\u{F50C}';
pub(crate) const Tab: char = '\u{F50D}';
pub(crate) const Space: char = '\u{F50E}';
pub(crate) const Menu: char = '\u{F50F}';
pub(crate) const PrintScreen: char = '\u{F510}';
pub(crate) const MAX_FUNCTION_KEY: u32 = 12;
pub(crate) fn function_key(n: u32) -> char {
assert!((1..=12).contains(&n));
char::from_u32(u32::from(Invalid) + n).unwrap()
assert!((1..=MAX_FUNCTION_KEY).contains(&n));
char::from_u32(u32::from('\u{F5FF}') - MAX_FUNCTION_KEY + (n - 1)).unwrap()
}
pub(crate) const Invalid: char = '\u{F5FF}';
const KEY_NAMES: &[(char, &wstr)] = &[
('-', L!("minus")),
@@ -47,6 +50,8 @@ pub(crate) fn function_key(n: u32) -> char {
(Insert, L!("insert")),
(Tab, L!("tab")),
(Space, L!("space")),
(Menu, L!("menu")),
(PrintScreen, L!("printscreen")),
];
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@@ -66,11 +71,22 @@ const fn new() -> Self {
sup: false,
}
}
#[cfg(test)]
pub(crate) const CTRL: Self = {
let mut m = Self::new();
m.ctrl = true;
m
};
pub(crate) const ALT: Self = {
let mut m = Self::new();
m.alt = true;
m
};
pub const SHIFT: Self = {
let mut m = Self::new();
m.shift = true;
m
};
pub(crate) fn is_some(&self) -> bool {
*self != Self::new()
}
@@ -86,39 +102,33 @@ pub struct Key {
}
impl Key {
pub(crate) fn from_raw(codepoint: char) -> Self {
pub(crate) const fn new(modifiers: Modifiers, codepoint: char) -> Self {
Self {
modifiers: Modifiers::default(),
modifiers,
codepoint,
}
}
pub(crate) fn from_raw(codepoint: char) -> Self {
Self::new(Modifiers::default(), codepoint)
}
}
pub(crate) const fn ctrl(codepoint: char) -> Key {
let mut modifiers = Modifiers::new();
modifiers.ctrl = true;
Key {
modifiers,
codepoint,
}
Key::new(modifiers, codepoint)
}
pub(crate) const fn alt(codepoint: char) -> Key {
let mut modifiers = Modifiers::new();
modifiers.alt = true;
Key {
modifiers,
codepoint,
}
Key::new(modifiers, codepoint)
}
pub(crate) const fn shift(codepoint: char) -> Key {
let mut modifiers = Modifiers::new();
modifiers.shift = true;
Key {
modifiers,
codepoint,
}
Key::new(modifiers, codepoint)
}
impl Key {
@@ -135,10 +145,7 @@ pub fn from_single_byte(c: u8) -> Self {
pub fn canonicalize_control_char(c: u8) -> Option<Key> {
let codepoint = canonicalize_keyed_control_char(char::from(c));
if u32::from(codepoint) > 255 {
return Some(Key {
modifiers: Modifiers::default(),
codepoint,
});
return Some(Key::from_raw(codepoint));
}
if c < 32 {
@@ -162,8 +169,10 @@ pub(crate) fn canonicalize_keyed_control_char(c: char) -> char {
if c == ' ' {
return Space;
}
if c == char::from(TERMINAL_MODE_ON_STARTUP.lock().unwrap().c_cc[VERASE]) {
return Backspace;
if let Some(tm) = safe_get_terminal_mode_on_startup() {
if c == char::from(tm.c_cc[VERASE]) {
return Backspace;
}
}
if c == char::from(127) {
// when it's not backspace
@@ -207,19 +216,6 @@ pub(crate) fn canonicalize_key(mut key: Key) -> Result<Key, WString> {
key.modifiers.ctrl = true;
}
}
if key.modifiers.shift {
if key.codepoint.is_ascii_alphabetic() {
// Shift + ASCII letters is just the uppercase letter.
key.modifiers.shift = false;
key.codepoint = key.codepoint.to_ascii_uppercase();
} else if !fish_is_pua(key.codepoint) {
// Shift + any other printable character is not allowed.
return Err(wgettext_fmt!(
"Shift modifier is only supported on special keys and lowercase ASCII, not '%s'",
key,
));
}
}
Ok(key)
}
@@ -289,25 +285,22 @@ pub(crate) fn parse_keys(value: &wstr) -> Result<Vec<Key>, WString> {
.find_map(|(codepoint, name)| (name == key_name).then_some(*codepoint))
.or_else(|| (key_name.len() == 1).then(|| key_name.as_char_slice()[0]));
let key = if let Some(codepoint) = codepoint {
canonicalize_key(Key {
modifiers,
codepoint,
})?
canonicalize_key(Key::new(modifiers, codepoint))?
} else if codepoint.is_none() && key_name.starts_with('f') && key_name.len() <= 3 {
let num = key_name.strip_prefix('f').unwrap();
let codepoint = match fish_wcstoi(num) {
Ok(n) if (1..=12).contains(&n) => function_key(u32::try_from(n).unwrap()),
let codepoint = match fish_wcstoul(num) {
Ok(n) if (1..=u64::from(MAX_FUNCTION_KEY)).contains(&n) => {
function_key(u32::try_from(n).unwrap())
}
_ => {
return Err(wgettext_fmt!(
"only f1 through f12 are supported, not 'f%s'",
"only f1 through f%d are supported, not 'f%s'",
MAX_FUNCTION_KEY,
num,
));
}
};
Key {
modifiers,
codepoint,
}
Key::new(modifiers, codepoint)
} else {
return Err(wgettext_fmt!(
"cannot parse key '%s'",
@@ -350,28 +343,6 @@ pub(crate) fn canonicalize_raw_escapes(keys: Vec<Key>) -> Vec<Key> {
canonical
}
impl Key {
pub(crate) fn codepoint_text(&self) -> Option<char> {
if self.modifiers.is_some() {
return None;
}
let c = self.codepoint;
if c == Space {
return Some(' ');
}
if c == Enter {
return Some('\n');
}
if c == Tab {
return Some('\t');
}
if fish_is_pua(c) || u32::from(c) <= 27 {
return None;
}
Some(c)
}
}
impl std::fmt::Display for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
WString::from(*self).fmt(f)
@@ -390,7 +361,7 @@ fn from(key: Key) -> Self {
.iter()
.find_map(|&(codepoint, name)| (codepoint == key.codepoint).then(|| name.to_owned()))
.or_else(|| {
(function_key(1)..=function_key(12))
(function_key(1)..=function_key(MAX_FUNCTION_KEY))
.contains(&key.codepoint)
.then(|| {
sprintf!(

View File

@@ -1,13 +1,13 @@
#include <locale.h>
#include <paths.h>
#include <paths.h> // _PATH_BSHELL
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdlib.h> // MB_CUR_MAX
#include <sys/mount.h> // MNT_LOCAL
#include <sys/resource.h>
#include <unistd.h>
#define UNUSED(x) (void)(x)
#include <sys/statvfs.h> // ST_LOCAL
#include <unistd.h> // _CS_PATH, _PC_CASE_SENSITIVE
size_t C_MB_CUR_MAX() { return MB_CUR_MAX; }

View File

@@ -218,17 +218,30 @@ fn parse_util_locate_cmdsub(
let mut last_dollar = None;
let mut paran_begin = None;
let mut paran_end = None;
enum Quote {
Real(char),
VirtualDouble,
}
fn process_opening_quote(
input: &[char],
inout_is_quoted: &mut Option<&mut bool>,
paran_count: i32,
quoted_cmdsubs: &mut Vec<i32>,
pos: usize,
mut pos: usize,
last_dollar: &mut Option<usize>,
quote: char,
quote: Quote,
) -> Option<usize> {
let quote = match quote {
Quote::Real(q) => q,
Quote::VirtualDouble => {
pos = pos.saturating_sub(1);
'"'
}
};
let q_end = quote_end(input.into(), pos, quote)?;
// Found a valid closing quote.
if input[q_end] == '$' {
// The closing quote is another quoted command substitution.
*last_dollar = Some(q_end);
quoted_cmdsubs.push(paran_count);
}
@@ -254,9 +267,9 @@ fn process_opening_quote(
&mut quoted_cmdsubs,
pos,
&mut last_dollar,
'"',
Quote::VirtualDouble,
)
.unwrap_or(input.len());
.map_or(input.len(), |pos| pos + 1);
}
while pos < input.len() {
@@ -270,7 +283,7 @@ fn process_opening_quote(
&mut quoted_cmdsubs,
pos,
&mut last_dollar,
c,
Quote::Real(c),
) {
Some(q_end) => pos = q_end,
None => break,
@@ -307,21 +320,19 @@ fn process_opening_quote(
if quoted_cmdsubs.last() == Some(&paran_count) {
quoted_cmdsubs.pop();
// Quoted command substitutions temporarily close double quotes.
// In "foo$(bar)baz$(qux)"
// We are here ^
// After the ) in a quoted command substitution, we need to act as if
// there was an invisible double quote.
match quote_end(input.into(), pos, '"') {
Some(q_end) => {
// Found a valid closing quote.
// Stop at $(qux), which is another quoted command substitution.
if input[q_end] == '$' {
quoted_cmdsubs.push(paran_count);
}
pos = q_end;
}
// In "foo$(bar)baz$(qux)", after the ), we need to act as if there was a double quote.
match process_opening_quote(
input,
&mut inout_is_quoted,
paran_count,
&mut quoted_cmdsubs,
pos,
&mut last_dollar,
Quote::VirtualDouble,
) {
Some(q_end) => pos = q_end,
None => break,
};
}
}
}
is_token_begin = is_token_delimiter(c, input.get(pos + 1).copied());

View File

@@ -614,7 +614,7 @@ pub fn eval_node<T: Node>(
let mut execution_context =
ExecutionContext::new(ps.clone(), block_io.clone(), Rc::clone(&line_counter));
terminal_protocols_disable_ifn();
terminal_protocols_disable_ifn(false);
// Check the exec count so we know if anything got executed.
let prev_exec_count = self.libdata().exec_count;

View File

@@ -2,12 +2,10 @@
//! for testing if a command with a given name can be found in the PATH, and various other
//! path-related issues.
use crate::common::{is_windows_subsystem_for_linux as is_wsl, wcs2osstring, wcs2zstring, WSL};
use crate::common::{wcs2osstring, wcs2zstring};
use crate::env::{EnvMode, EnvStack, Environment};
use crate::expand::{expand_tilde, HOME_DIRECTORY};
use crate::flog::{FLOG, FLOGF};
#[cfg(not(target_os = "linux"))]
use crate::libc::{MNT_LOCAL, ST_LOCAL};
use crate::wchar::prelude::*;
use crate::wutil::{normalize_path, path_normalize_for_cd, waccess, wdirname, wstat};
use errno::{errno, set_errno, Errno};
@@ -15,6 +13,7 @@
use once_cell::sync::Lazy;
use std::ffi::OsStr;
use std::io::ErrorKind;
use std::mem::MaybeUninit;
use std::os::unix::prelude::*;
/// Returns the user configuration directory for fish. If the directory or one of its parents
@@ -308,29 +307,6 @@ fn path_get_path_core<S: AsRef<wstr>>(cmd: &wstr, pathsv: &[S]) -> GetPathResult
return GetPathResult::new(test_path(cmd).err(), cmd.to_owned());
}
// WSLv1/WSLv2 tack on the entire Windows PATH to the end of the PATH environment variable, and
// accessing these paths from WSL binaries is pathalogically slow. We also don't expect to find
// any "normal" nix binaries under these paths, so we can skip them unless we are executing bins
// with Windows-ish names. We try to keep paths manually added to $fish_user_paths by only
// chopping off entries after the last "normal" PATH entry.
let pathsv = if is_wsl(WSL::Any) && !cmd.contains('.') {
let win_path_count = pathsv
.iter()
.rev()
.take_while(|p| {
let p = p.as_ref();
p.starts_with("/mnt/")
&& p.chars()
.nth("/mnt/x".len())
.map(|c| c == '/')
.unwrap_or(false)
})
.count();
&pathsv[..pathsv.len() - win_path_count]
} else {
pathsv
};
let mut best = noent_res;
for next_path in pathsv {
let next_path: &wstr = next_path.as_ref();
@@ -699,10 +675,11 @@ fn path_remoteness(path: &wstr) -> DirRemoteness {
let narrow = wcs2zstring(path);
#[cfg(target_os = "linux")]
{
let mut buf: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statfs(narrow.as_ptr(), &mut buf) } < 0 {
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() };
// Linux has constants for these like NFS_SUPER_MAGIC, SMB_SUPER_MAGIC, CIFS_MAGIC_NUMBER but
// these are in varying headers. Simply hard code them.
// Note that we treat FUSE filesystems as remote, which means we lock less on such filesystems.
@@ -732,38 +709,49 @@ fn path_remoteness(path: &wstr) -> DirRemoteness {
}
#[cfg(not(target_os = "linux"))]
{
let st_local = ST_LOCAL();
if st_local != 0 {
// ST_LOCAL is a flag to statvfs, which is itself standardized.
// In practice the only system to use this path is NetBSD.
let mut buf: libc::statvfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statvfs(narrow.as_ptr(), &mut buf) } < 0 {
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;
}
// statvfs::f_flag is `unsigned long`, which is 4-bytes on most 32-bit targets.
#[cfg_attr(target_pointer_width = "64", allow(clippy::useless_conversion))]
return if u64::from(buf.f_flag) & st_local != 0 {
DirRemoteness::local
} else {
DirRemoteness::remote
};
}
let mnt_local = MNT_LOCAL();
if mnt_local != 0 {
let mut buf: libc::statvfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statvfs(narrow.as_ptr(), &mut buf) } < 0 {
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)]
return if u64::from(buf.f_flag) & mnt_local != 0 {
if u64::from((flagsfn)(&buf)) & is_local_flag != 0 {
DirRemoteness::local
} else {
DirRemoteness::remote
};
}
}
DirRemoteness::unknown
// 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

@@ -34,8 +34,7 @@
use std::rc::Rc;
#[cfg(target_has_atomic = "64")]
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::atomic::{AtomicI32, AtomicU32, AtomicU8};
use std::sync::atomic::{AtomicI32, AtomicPtr, AtomicU32, AtomicU8, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant};
@@ -67,6 +66,7 @@
use crate::flog::{FLOG, FLOGF};
#[allow(unused_imports)]
use crate::future::IsSomeAnd;
use crate::future_feature_flags::{feature_test, FeatureFlag};
use crate::global_safety::RelaxedAtomicBool;
use crate::highlight::{
autosuggest_validate_from_history, highlight_shell, HighlightRole, HighlightSpec,
@@ -152,9 +152,9 @@ enum ExitState {
pub static SHELL_MODES: Lazy<Mutex<libc::termios>> =
Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() }));
/// Mode on startup, which we restore on exit.
pub static TERMINAL_MODE_ON_STARTUP: Lazy<Mutex<libc::termios>> =
Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() }));
/// The valid terminal modes on startup. This is set once and not modified after.
/// Warning: this is read from the SIGTERM handler! Hence the raw global.
static TERMINAL_MODE_ON_STARTUP: AtomicPtr<libc::termios> = AtomicPtr::new(std::ptr::null_mut());
/// Mode we use to execute programs.
static TTY_MODES_FOR_EXTERNAL_CMDS: Lazy<Mutex<libc::termios>> =
@@ -171,6 +171,12 @@ enum ExitState {
/// This is set from a signal handler.
static SIGHUP_RECEIVED: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
// Get the terminal mode on startup. This is "safe" because it's async-signal safe.
pub fn safe_get_terminal_mode_on_startup() -> Option<&'static libc::termios> {
// Safety: set atomically and not modified after.
unsafe { TERMINAL_MODE_ON_STARTUP.load(Ordering::Acquire).as_ref() }
}
/// A singleton snapshot of the reader state. This is factored out for thread-safety reasons:
/// it may be fetched on a background thread.
fn commandline_state_snapshot() -> MutexGuard<'static, CommandlineState> {
@@ -659,13 +665,15 @@ fn read_i(parser: &Parser) -> i32 {
data.command_line.clear();
data.update_buff_pos(EditableLineTag::Commandline, None);
data.command_line_changed(EditableLineTag::Commandline);
// OSC 133 "Command start"
write!(
BufferedOuputter::new(&mut Outputter::stdoutput().borrow_mut()),
"\x1b]133;C;cmdline_url={}\x07",
escape_string(&command, EscapeStringStyle::Url),
)
.unwrap();
if feature_test(FeatureFlag::mark_prompt) {
// OSC 133 "Command start"
write!(
BufferedOuputter::new(&mut Outputter::stdoutput().borrow_mut()),
"\x1b]133;C;cmdline_url={}\x07",
escape_string(&command, EscapeStringStyle::Url),
)
.unwrap();
}
event::fire_generic(parser, L!("fish_preexec").to_owned(), vec![command.clone()]);
let eval_res = reader_run_command(parser, &command);
signal_clear_cancel();
@@ -678,12 +686,14 @@ fn read_i(parser: &Parser) -> i32 {
parser.libdata_mut().exit_current_script = false;
// OSC 133 "Command finished"
write!(
BufferedOuputter::new(&mut Outputter::stdoutput().borrow_mut()),
"\x1b]133;D;{}\x07",
parser.get_last_status()
)
.unwrap();
if feature_test(FeatureFlag::mark_prompt) {
write!(
BufferedOuputter::new(&mut Outputter::stdoutput().borrow_mut()),
"\x1b]133;D;{}\x07",
parser.get_last_status()
)
.unwrap();
}
event::fire_generic(parser, L!("fish_postexec").to_owned(), vec![command]);
// Allow any pending history items to be returned in the history array.
data.history.resolve_pending();
@@ -813,8 +823,15 @@ fn read_ni(parser: &Parser, fd: RawFd, io: &IoChain) -> i32 {
/// Initialize the reader.
pub fn reader_init(will_restore_foreground_pgroup: bool) {
// Save the initial terminal mode.
let mut terminal_mode_on_startup = TERMINAL_MODE_ON_STARTUP.lock().unwrap();
unsafe { libc::tcgetattr(STDIN_FILENO, &mut *terminal_mode_on_startup) };
// Note this field is read by a signal handler, so do it atomically, with a leaked mode.
let mut terminal_mode_on_startup = unsafe { std::mem::zeroed::<libc::termios>() };
let ret = unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut terminal_mode_on_startup) };
// TODO: rationalize behavior if initial tcgetattr() fails.
if ret == 0 {
// Must be mut because AtomicPtr doesn't have const variant.
let leaked: *mut libc::termios = Box::leak(Box::new(terminal_mode_on_startup));
TERMINAL_MODE_ON_STARTUP.store(leaked, Ordering::Release);
}
#[cfg(not(test))]
assert!(AT_EXIT.get().is_none());
@@ -826,7 +843,7 @@ pub fn reader_init(will_restore_foreground_pgroup: bool) {
// Set the mode used for program execution, initialized to the current mode.
let mut tty_modes_for_external_cmds = TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap();
*tty_modes_for_external_cmds = *terminal_mode_on_startup;
*tty_modes_for_external_cmds = terminal_mode_on_startup;
term_fix_external_modes(&mut tty_modes_for_external_cmds);
// Disable flow control by default.
@@ -838,7 +855,6 @@ pub fn reader_init(will_restore_foreground_pgroup: bool) {
term_fix_modes(&mut shell_modes());
drop(terminal_mode_on_startup);
drop(tty_modes_for_external_cmds);
// Set up our fixed terminal modes once,
@@ -850,9 +866,11 @@ pub fn reader_init(will_restore_foreground_pgroup: bool) {
}
}
// TODO(pca): this is run in our "AT_EXIT" handler from a SIGTERM handler.
// It must be made async-signal-safe (or not invoked).
pub fn reader_deinit(in_signal_handler: bool, restore_foreground_pgroup: bool) {
restore_term_mode(in_signal_handler);
crate::input_common::terminal_protocols_disable_ifn();
safe_restore_term_mode(in_signal_handler);
crate::input_common::terminal_protocols_disable_ifn(in_signal_handler);
if restore_foreground_pgroup {
restore_term_foreground_process_group_for_exit();
}
@@ -861,20 +879,15 @@ pub fn reader_deinit(in_signal_handler: bool, restore_foreground_pgroup: bool) {
/// Restore the term mode if we own the terminal and are interactive (#8705).
/// It's important we do this before restore_foreground_process_group,
/// otherwise we won't think we own the terminal.
pub fn restore_term_mode(in_signal_handler: bool) {
/// THIS FUNCTION IS CALLED FROM A SIGNAL HANDLER. IT MUST BE ASYNC-SIGNAL-SAFE.
pub fn safe_restore_term_mode(in_signal_handler: bool) {
if !is_interactive_session() || unsafe { libc::getpgrp() != libc::tcgetpgrp(STDIN_FILENO) } {
return;
}
if unsafe {
libc::tcsetattr(
STDIN_FILENO,
TCSANOW,
&*TERMINAL_MODE_ON_STARTUP.lock().unwrap(),
) == -1
} && errno().0 == EIO
{
redirect_tty_output(in_signal_handler);
if let Some(modes) = safe_get_terminal_mode_on_startup() {
if unsafe { libc::tcsetattr(STDIN_FILENO, TCSANOW, modes) == -1 } && errno().0 == EIO {
redirect_tty_output(in_signal_handler);
}
}
}
@@ -980,7 +993,7 @@ pub fn reader_execute_readline_cmd(parser: &Parser, ch: CharEvent) {
data.rls = Some(ReadlineLoopState::new());
}
data.save_screen_state();
data.handle_char_event(Some(ch));
let _ = data.handle_char_event(Some(ch));
}
}
@@ -5812,7 +5825,7 @@ fn compute_and_apply_completions(&mut self, c: ReadlineCmd) {
token_range.end += cmdsub_range.start;
// Wildcard expansion and completion below check for cancellation.
terminal_protocols_disable_ifn();
terminal_protocols_disable_ifn(false);
// Check if we have a wildcard within this string; if so we first attempt to expand the
// wildcard; if that succeeds we don't then apply user completions (#8593).

View File

@@ -7,6 +7,7 @@
//! The current implementation is less smart than ncurses allows and can not for example move blocks
//! of text around to handle text insertion.
use crate::future_feature_flags::{feature_test, FeatureFlag};
use crate::pager::{PageRendering, Pager, PAGER_MIN_HEIGHT};
use std::cell::RefCell;
use std::collections::LinkedList;
@@ -931,8 +932,11 @@ fn update(
} else if left_prompt != zelf.actual_left_prompt || (zelf.scrolled && is_final_rendering) {
zelf.r#move(0, 0);
let mut start = 0;
let osc_133_prompt_start =
|zelf: &mut Screen| zelf.write_bytes(b"\x1b]133;A;special_key=1\x07");
let osc_133_prompt_start = |zelf: &mut Screen| {
if feature_test(FeatureFlag::mark_prompt) {
zelf.write_bytes(b"\x1b]133;A;special_key=1\x07");
}
};
if left_prompt_layout.line_breaks.is_empty() {
osc_133_prompt_start(&mut zelf);
}

View File

@@ -1,6 +1,6 @@
use crate::env::EnvStack;
use crate::input::{EventQueuePeeker, InputMappingSet, KeyNameStyle, DEFAULT_BIND_MODE};
use crate::input_common::{CharEvent, InputData, InputEventQueuer};
use crate::input_common::{CharEvent, InputData, InputEventQueuer, KeyEvent};
use crate::key::Key;
use crate::wchar::prelude::*;
use std::rc::Rc;
@@ -52,8 +52,10 @@ fn test_input() {
);
// Push the desired binding to the queue.
for c in desired_binding {
input.input_data.queue_char(CharEvent::from_key(c));
for key in desired_binding {
input
.input_data
.queue_char(CharEvent::from_key(KeyEvent::from(key)));
}
let mut peeker = EventQueuePeeker::new(&mut input);

View File

@@ -439,5 +439,10 @@ macro_rules! validate {
0, r#"echo "$()"'"#,
0, "\n"
);
validate!(
0, r#"""#,
0, "\n",
0, r#"$()"$() ""#
);
})();
}

View File

@@ -1,11 +1,15 @@
use std::collections::HashMap;
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::sync::Mutex;
use crate::common::{charptr2wcstring, truncate_at_nul, wcs2zstring, PACKAGE_NAME};
#[cfg(test)]
use crate::tests::prelude::*;
use crate::wchar::prelude::*;
use crate::{
common::{charptr2wcstring, truncate_at_nul, wcs2zstring, PACKAGE_NAME},
env::CONFIG_PATHS,
};
use errno::{errno, set_errno};
use once_cell::sync::{Lazy, OnceCell};
@@ -47,8 +51,11 @@ pub fn fish_textdomain(_domainname: &CStr) -> *mut c_char {
// Really init wgettext.
fn wgettext_really_init() {
let Some(ref localepath) = CONFIG_PATHS.locale else {
return;
};
let package_name = CString::new(PACKAGE_NAME).unwrap();
let localedir = CString::new(env!("LOCALEDIR")).unwrap();
let localedir = CString::new(localepath.as_os_str().as_bytes()).unwrap();
fish_bindtextdomain(&package_name, &localedir);
fish_textdomain(&package_name);
}

View File

@@ -0,0 +1,21 @@
#RUN: %fish %s
set -g fish_complete_path c1 c2
mkdir -p c1 c2
function foo; end
for i in c1 c2
echo >$i/foo.fish "echo auto-loading $i/foo.fish"
end
complete -C "foo " >/dev/null
# CHECK: auto-loading c1/foo.fish
complete -C "foo " >/dev/null
# already loaded
set -g fish_complete_path c2
complete -C "foo " >/dev/null
# CHECK: auto-loading c2/foo.fish
set -g fish_complete_path c1 c2
complete -C "foo " >/dev/null
# CHECK: auto-loading c1/foo.fish

View File

@@ -151,4 +151,19 @@ bind \n 2>&1
bind _\cx_\ci_\ei_\\_\'_ 'echo foo'
# CHECKERR: bind: cannot parse key '_\cx_\t_\ei_\\_'_'
bind A
# CHECKERR: bind: No binding found for key 'A'
bind shift-a
# CHECKERR: bind: No binding found for key 'shift-a'
bind shift-A
# CHECKERR: bind: No binding found for key 'shift-A'
bind ctrl-shift-a
# CHECKERR: bind: No binding found for key 'ctrl-shift-a'
bind ctrl-shift-ä
# CHECKERR: bind: No binding found for key 'ctrl-shift-ä'
exit 0

View File

@@ -627,3 +627,22 @@ complete -C'testcommand '
# CHECK: check{{\t}}Check the frobnicator
# CHECK: search{{\t}}Search for frobs
# CHECK: show{{\t}}Show all frobs
complete complete-list -xa '(__fish_complete_list , "seq 2")'
complete -C "complete-list 1,"
# CHECK: 1,1
# CHECK: 1,2
complete complete-list -s l -l number-list -xa '(__fish_stripprefix="^(--number-list=|-\w*l)" __fish_complete_list , "seq 2")'
complete -C "complete-list --number-list=1,"
# CHECK: --number-list=1,1
# CHECK: --number-list=1,2
complete -C "complete-list -abcl1,"
# CHECK: -abcl1,1
# CHECK: -abcl1,2
function esc_in_description
echo completion\t'escaped \n newline'
end
complete complete-list -l desc -xa '(__fish_complete_list , esc_in_description)'
complete -C 'complete-list --desc '
# CHECK: completion{{\t}}escaped {{\\n}} newline

View File

@@ -18,3 +18,16 @@ or echo "pgroups were the same, job control did not work"
$fish -c 'status job-control full ; $fth report_foreground' &
wait
#CHECKERR: background
$fish -c '
function __fish_print_help
if set -q argv[2]
echo Error-message: $argv[2] >&2
end
echo Documentation for $argv[1] >&2
return 1
end
sleep .2 & bg %1
'
#CHECKERR: Error-message: bg: Can't put job 1, 'sleep .2 &' to background because it is not under job control
#CHECKERR: Documentation for bg

View File

@@ -154,3 +154,21 @@ printf '%b\n' '\0057foo\0057bar\0057'
printf %18446744073709551616s
# CHECKERR: Number out of range
# Test non-ASCII behavior
printf '|%3s|\n' 'ö'
# CHECK: | ö|
printf '|%3s|\n' '🇺🇳'
#CHECK: | 🇺🇳|
printf '|%.3s|\n' '🇺🇳🇺🇳'
#CHECK: |🇺🇳|
printf '|%.3s|\n' 'a🇺🇳'
#CHECK: |a🇺🇳|
printf '|%.3s|\n' 'aa🇺🇳'
#CHECK: |aa|
printf '|%3.3s|\n' 'aa🇺🇳'
#CHECK: | aa|
printf '|%.1s|\n' '𒈙a'
#CHECK: |𒈙|
printf '|%3.3s|\n' '👨‍👨‍👧‍👧'
#CHECK: | 👨‍👨‍👧‍👧|

View File

@@ -0,0 +1,16 @@
#RUN: %fish %s
#REQUIRES: command -v sphinx-build
#REQUIRES: python -c 'import sphinx_markdown_builder'
set -l workspace_root (status dirname)/../..
if not test -e $workspace_root/.git
return
end
$workspace_root/build_tools/release-notes.sh -q >?relnotes.md
or {
echo "Failed to build Markdown release notes."
return
}
sed -n 1p relnotes.md | grep -q '^## fish \S* (released .*)'
or echo "Unexpected changelog title"

View File

@@ -38,6 +38,9 @@ status --job-control=1none
# Now set it to a valid mode.
status job-control none
status current-command | sed s/^/^/
# CHECK: ^fish
# Check status -u outside functions
status current-function
#CHECK: Not a function
@@ -60,6 +63,7 @@ status features
#CHECK: remove-percent-self off 4.0 %self is no longer expanded (use $fish_pid)
#CHECK: test-require-arg off 4.0 builtin test requires an argument
#CHECK: keyboard-protocols on 4.0 Use keyboard protocols (kitty, xterm's modifyotherkeys
#CHECK: mark-prompt on 4.0 Write OSC 133 prompt markers to the terminal
status test-feature stderr-nocaret
echo $status
#CHECK: 0

View File

@@ -0,0 +1,21 @@
#RUN: %fish %s
#REQUIRES: command -v tmux
set -g isolated_tmux_fish_extra_args -C '
set -g fish_key_bindings fish_vi_key_bindings
'
isolated-tmux-start
isolated-tmux send-keys 'echo 124' Escape
tmux-sleep # disambiguate escape from alt
isolated-tmux send-keys v b y p i 3
tmux-sleep
isolated-tmux capture-pane -p
# CHECK: [I] prompt 0> echo 1241234
isolated-tmux send-keys Escape
tmux-sleep # disambiguate escape from alt
isolated-tmux send-keys e r 5
tmux-sleep
isolated-tmux capture-pane -p
# CHECK: [N] prompt 0> echo 1241235

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
from pexpect_helper import SpawnedProc
from pexpect_helper import SpawnedProc, control
sp = SpawnedProc()
send, sendline, sleep, expect_prompt, expect_re, expect_str = (
@@ -78,3 +78,15 @@ sendline("echo bar")
expect_re("\n.*bar")
sendline("echo fo\t")
expect_re("foooo")
# Custom completions that access the command line.
sendline("complete -e :; complete : -a '(echo (commandline -ct)-completed)'")
send(": abcd" + control("b") * 2 + "\t")
expect_str(": abcd-completed")
send(control("u"))
# Another one.
sendline("mkdir -p foo/bar; touch foo/bar/baz.fish")
send("source foo/b/baz.fish")
send(control("b") * 9 + "\t")
expect_str("source foo/bar/baz.fish")
send(control("u"))