Skip to content

mv rework, native tail, and fixes - #116

Merged
harendra-kumar merged 14 commits into
masterfrom
updates
Oct 21, 2026
Merged

harendra-kumar merged 14 commits into
masterfrom
updates

Conversation

@harendra-kumar

Copy link
Copy Markdown
Member

Mostly brings mv in line with GNU mv:

  • mv now replaces an existing destination by default, and moves the source into the destination when the destination is a directory. force is replaced by overwrite, which uses the Overwrite type shared with cp (renamed from CpOverwrite).
  • OverwriteNever now skips silently, like --update=none, and is atomic on Linux, macOS and Windows. New OverwriteError fails instead of replacing, like --update=none-fail.
  • New mvMany moves several sources into a directory, like mv -t.

Also:

  • tail is implemented natively instead of running the tail program.
  • ln with force now replaces the destination, like ln -f.
  • On Windows, Sh uses sh from the PATH.
  • Requires streamly-core 0.3.2.
  • A new limit-build-mem flag caps the heap used by GHC and the tests.
  • hlint is now 3.10.
  • The CI configs are synced with the packcheck templates.

streamly-core 0.3.0 Path.fromString keeps only as many bytes as there
are characters, corrupting non-ASCII paths; 0.3.1 fixes that.
File.fromChunks in 0.3.1 does not truncate an existing file opened in
WriteMode; 0.3.2 fixes that. streamly 0.11.1 revision 2 allows
streamly-core 0.3.2. Use these versions in the nix flake and stack.yaml.
ln (force True) skipped the existence check but did not remove the
destination, so creating the link failed with "File exists". Like GNU
ln -f, remove the destination first, and refuse when the source and
the destination are the same directory entry. A dangling symlink at the
destination now also counts as existing.
Add a manual limit-build-mem flag, off by default so that users of the
library are not affected, and enable it in the project files.
Read a regular file backwards from the end in 64 KiB blocks, so only
the output part is read. Read a pipe or FIFO to the end, keeping in
memory only the chunks that can contain the output. follow polls the
file size once a second and starts again from the beginning when the
file is truncated. The output matches GNU tail for -n, -c, -n +K and
-c +K on regular files and FIFOs. tail no longer needs the tail program
and uses only portable APIs (not yet tested on Windows).

followHandle used Stream.append recursively once per poll, and dropLines
and dropBytes recursed through Stream.concatEffect and Stream.cons. Fused
streams are not meant for recursive use.

followHandle now uses Stream.repeatM with Stream.concatMap. dropLines and
dropBytes share dropPrefix, a single scan followed by mapMaybeM.

Mark the stream helpers INLINE so that tail fuses without the fusion
plugin.
/bin/sh does not exist on Windows. Run commands with sh found on the
PATH there, e.g. from Git for Windows, MSYS2 or Cygwin, so that the same
POSIX shell commands work on all platforms. cmd.exe is not used because
its syntax and quoting differ.

Say in the README and the cabal description that Sh is the exception to
running no subprocesses, and which shell it uses on POSIX and on Windows.
GNU mv replaces an existing destination by default. Replace Mv.force
with Mv.overwrite, taking the Overwrite type now shared with cp
(CpOverwrite renamed to Overwrite, cpOverwrite to overwrite).
OverwriteNever skips silently like --update=none instead of raising
an error.
@harendra-kumar
harendra-kumar force-pushed the updates branch 2 times, most recently from 8f957dd to 53773f4 Compare October 20, 2026 23:09
mv moves the source into dest when dest is an existing directory or a
symlink to one, like GNU mv. intoDirectory False gives mv -T. mvMany
moves several sources into a directory, like mv -t. Like GNU mv, it
tries every source and then raises one IOError listing each failure,
instead of stopping at the first failure.
Use renameat2 with RENAME_NOREPLACE on Linux, renamex_np with
RENAME_EXCL on macOS and MoveFileExW without MOVEFILE_REPLACE_EXISTING
on Windows, so a destination created concurrently is never replaced.
Other platforms and file systems without support fall back to checking
the destination before the rename, like gnulib.

On Windows the C stub returns GetLastError() so that nothing can
overwrite it before Haskell reads it. The paths are converted to the
\\?\ form as renamePath in directory does, so that paths longer than
MAX_PATH work with mv OverwriteNever.
Like GNU --update=none-fail. mv uses the atomic no-replace rename for
it, as for OverwriteNever.
Bump packcheck to 20514de in both configs. In the GitHub workflow,
select the default Xcode on macOS and point the CLT SDK at it, include
the Xcode version in the GHC and dependency cache keys, rename the
hackage index cache key to cabal-hackage-index and add /ucrt64/bin to
the Windows PATH. In appveyor, disable the sdist build.

Lint the benchmark directory instead of the nonexistent Benchmark. The
new packcheck runs hlint on every target and fails on a missing one.
Run the FreeBSD build in a VM via vmactions/freebsd-vm, as in streamly,
and remove .cirrus.yml.
Root bypasses directory permission bits, so removing a file under a
parent without write or execute permission succeeds, as with GNU rm.
These tests failed on the FreeBSD CI, where the VM runs them as root.
@harendra-kumar
harendra-kumar merged commit 25daf24 into master Oct 21, 2026
13 of 14 checks passed
@harendra-kumar
harendra-kumar deleted the updates branch October 21, 2026 23:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant