A Unix-like find command for Windows, written in Go.
Rename the binary to anything you like — all usage and error messages derive from the executable name automatically.
Requires Go 1.21+.
go install github.com/fermat-tech/winfind@latestThe binary is placed in your $GOPATH/bin (usually %USERPROFILE%\go\bin), which should already be on your PATH.
Grab the latest binary from Releases and put it somewhere on your PATH.
git clone https://github.com/fermat-tech/winfind.git
cd winfind
go build -o winfind.exe .winfind [path...] [expression]
Paths default to . if omitted. -h / --help prints usage and --version
prints version information.
| Flag | Description |
|---|---|
-name PATTERN |
Filename glob (case-sensitive) |
-iname PATTERN |
Filename glob (case-insensitive) |
-path PATTERN |
Full path glob |
-ipath PATTERN |
Full path glob (case-insensitive) |
-type f|d|l |
File, directory, or symlink |
-size [+/-]N[ckMG] |
Size filter — c=bytes, k=KB, M=MB, G=GB; +N means greater than, -N means less than |
-mtime [+/-]N |
Modified N days ago; +N=older than, -N=newer than |
-newer FILE |
Modified more recently than FILE |
-empty |
Empty file or empty directory |
-maxdepth N |
Descend at most N directory levels |
-mindepth N |
Skip entries fewer than N levels deep |
-name, -iname, -path and -ipath take fnmatch(3) globs — the same ones
GNU find uses:
* |
any run of characters, including none |
? |
any single character |
[abc], [a-z] |
any one of the listed characters |
[!abc], [^abc] |
any character not listed |
[[:digit:]] |
a POSIX class: alpha digit alnum upper lower space blank punct print graph cntrl xdigit |
* and ? cross directory separators, as in GNU find, so -path 'src*main.go'
matches src\cmd\main.go. A malformed pattern — a trailing \, or a [ with no
closing ] — matches nothing rather than reporting an error, which is what glibc
does. -iname and -ipath fold case by lowercasing both sides, so a range like
[A-Z] becomes [a-z] under them.
A backslash means one thing to -name and another to -path, because on Windows
the two need different things:
-name, -iname |
\ escapes the next character, as in GNU find. -name "*\.yml" matches a literal dot, and -name "v\*.c" finds a file named v*.c. A Windows filename cannot contain a separator, so a backslash here can only have been meant as an escape. |
-path, -ipath |
\ is a path separator, not an escape, so -path "*\sub\*" means what it plainly says. Both separators are accepted and mean the same thing: -path "*\sub\*" and -path "*/sub/*" match the same files. |
The -path rule is a deliberate divergence from GNU find, which reads
-path '*\sub\*' as the escaped letters *sub*. -path matches the path as
winfind prints it, and on Windows that path is spelled with backslashes.
One consequence worth knowing: paths print without a ./ prefix, so a pattern
anchored at the top of the tree needs no leading *. When sub sits directly
under the starting point, use -path "sub\*" — -path "*\sub\*" finds nothing,
because there is nothing ahead of sub for the * to match.
Higher to lower precedence:
! PRED negate
-not PRED negate (long form)
PRED1 PRED2 implicit AND
PRED1 -and PRED2 explicit AND
PRED1 -or PRED2 OR
( PRED ) grouping
If no action is specified, -print is the default.
| Flag | Description |
|---|---|
-print |
Print path followed by newline |
-print0 |
Print path followed by null byte (for use with xargs -0) |
-ls |
Print size, mode, modification time, and path |
-delete |
Delete matched file or empty directory |
-exec CMD {} ; |
Run CMD for each match — {} is replaced by the path |
-printf FORMAT |
Print FORMAT for each match, expanding \ escapes and % directives |
-printf does not add a trailing newline — end the format with \n if you
want one. Field widths and precisions work as in C's printf, so %-40p left-
justifies the path in a 40-column field and %.10f truncates the basename to
ten characters.
%p path |
%f basename |
%h leading directories |
%P path with the starting point removed |
%H the starting point |
%d depth below the starting point (0 = the starting point) |
%s size in bytes |
%b disk space in 512-byte blocks |
%k disk space in 1 KiB blocks |
%S sparseness (allocated ÷ apparent) |
%n hard link count |
%i file index (see below) |
%D volume serial number |
%F file system type |
%l symlink or junction target |
%m permission bits, octal |
%M permissions, ls -l style |
%y type letter |
%Y type letter, following links |
%u owner name |
%U owner SID |
%g group name |
%G group SID |
%Z SELinux context (always empty) |
%t modification time |
%a access time |
%c creation time |
%% a literal % |
Type letters for %y are f regular, d directory, l link, p FIFO,
s socket, c character device, b block device, ? unknown. %Y resolves
the link and reports the target's letter instead, or N if the target is
missing, L if the links form a cycle, and ? for any other failure.
%t, %a and %c print a ctime(3)-style stamp with a fractional second:
Wed Apr 28 22:22:05.1234567000 2004.
%Tk, %Ak, %Ck and %Bk print the modification, access, creation and
birth time using a single format character k:
@ |
seconds since the epoch, with a fractional part |
H I k l M p r S T + X Z |
time fields |
a A b B c d D e F h j m U w W x y Y |
date fields |
These follow strftime(3). On Windows %c and %B are the same value, since
the creation time is the birth time.
\a \b \f \n \r \t \v \\, \NNN for an octal character code,
and \c to stop printing that format immediately and flush.
-printf is matched against GNU findutils 4.8.0. Where Windows has no
equivalent of a Unix concept, winfind reports the closest thing the platform
actually knows rather than inventing a number:
| Directive | On Windows |
|---|---|
%p %h |
The starting point is cleaned, so winfind . prints a.txt, not ./a.txt. This matches what -print has always done. |
%m %M |
Windows has no POSIX permission bits. Files read as 666 (or 444 when read-only) and directories as 777. |
%u %g |
The account name as DOMAIN\name, or the SID when the account cannot be resolved. |
%U %G |
Windows has no numeric uid or gid, so the SID string stands in. |
%c |
Windows has no status-change time. The creation time is reported instead — the same convention MSVC's stat() follows. |
%B |
Genuinely available here, unlike on most Linux file systems. |
%i |
The NTFS file index: unique per volume, and it serves the same purpose as an inode number. |
%D |
The volume serial number rather than a device number. |
%b %k %S |
Taken from the file's allocated size, so they agree with Explorer's "size on disk". Expect smaller numbers than Linux for tiny files: NTFS stores those inside the MFT record instead of allocating a cluster. |
%y %Y %l |
Junctions and directory mount points report as l. Go calls them irregular files, but a junction is a link to a directory. |
%Z |
Always empty; SELinux has no counterpart. |
%Tc %Tx %TX %Tr %Tp |
Fixed C/en_US forms — winfind has no locale support. |
Fractional seconds are ten digits wide as in GNU find, but NTFS timestamps have 100 ns resolution, so the last three digits are always zero.
# Find all Go source files
winfind . -name "*.go"
# Find files larger than 1 MB
winfind C:\src -type f -size +1M
# Find log files modified in the last 7 days and delete them
winfind . -mtime -7 -name "*.log" -delete
# List all files with details
winfind . -type f -ls
# Escape a metacharacter: a file actually named v*.c
winfind . -name "v\*.c"
# Everything under a sub-directory, either separator
winfind . -path "src\cmd\*"
# Find files NOT matching a pattern
winfind . -not -name "*.exe"
# Combine conditions with OR
winfind . -iname "*.jpg" -or -iname "*.png"
# Run a command on each match
winfind . -name "*.txt" -exec notepad {} ;
# Limit search depth
winfind . -maxdepth 2 -type f
# Find empty directories
winfind . -type d -empty
# Newest files first
winfind . -type f -printf "%T@ %p\n" | winsort -rn | wincut -d' ' -f2-
# Size and path, tab separated, largest first
winfind . -type f -printf "%s\t%p\n" | winsort -rn
# A column layout of your own
winfind . -type f -printf "%-40p %8s %TY-%Tm-%Td\n"
# Null-separated paths for xargs -0, via -printf
winfind . -name "*.log" -printf "%p\0"MIT