Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ shell/gitprompt-linux-amd64
shell/gitprompt-linux-arm64
shell/gitprompt-windows-arm64.exe
shell/pass
shell/sleep
*/*.exe
51 changes: 47 additions & 4 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,55 @@ sudo cp shell/gitprompt /usr/local/bin/

---

## sleep - Windows sleep(1)

A minimal GNU-`sleep`-alike for Windows, which ships no `sleep` of its own.
Written in C, ~13 KB, no runtime dependencies.

Motivation: `timeout /t N` is integer-only and fails with "Input redirection
is not supported" whenever stdin is not a console — for example when called
from the [goal](https://codeberg.org/anaseto/goal) interpreter's `run`
builtin. See `specs/009-sleep-command/spec.md`.

### Build
```bash
cd sleep && ./build.sh # native build → ../shell/sleep[.exe]
cd sleep && ./build.sh clean
```

### Install
```bash
# Windows: copy sleep.exe to a directory on %PATH%
copy shell\sleep.exe C:\Dev\bins\

# Linux/macOS: not needed, coreutils already provides sleep
```

### Usage
```bash
sleep 2 # 2 seconds
sleep 0.25 # fractional seconds
sleep 1.5s # explicit suffix
sleep 2m # minutes; also h (hours), d (days)
sleep 1m 30 # arguments are summed → 90 seconds
```

From goal:
```k
run"sleep""0.25"
```

### Prerequisites
- A C compiler (`gcc`, `clang`); override with `CC=...`

---

## Building All Tools

Use the wrapper script to build both tools at once:
Use the wrapper script to build all tools at once:
```bash
./shell/make.sh # build both gitprompt and pass
./shell/make.sh # build gitprompt, pass and sleep
./shell/make.sh build # same
./shell/make.sh build-all # cross-compile both
./shell/make.sh clean # clean both builds
./shell/make.sh build-all # cross-compile (sleep is native-only)
./shell/make.sh clean # clean all builds
```
14 changes: 9 additions & 5 deletions shell/make.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/bin/sh
# Wrapper script to build both gitprompt and pass.
# Wrapper script to build all tools: gitprompt, pass and sleep.
#
# Usage:
# ./make.sh # build both tools
# ./make.sh # build all tools
# ./make.sh build # same
# ./make.sh build-all # cross-compile both tools
# ./make.sh clean # clean both builds
# ./make.sh update-deps # update dependencies for both
# ./make.sh build-all # cross-compile (sleep is native-only)
# ./make.sh clean # clean all builds
# ./make.sh update-deps # update dependencies where applicable

set -eu

Expand All @@ -25,11 +25,15 @@ case "$cmd" in

echo "Building pass..."
(cd "$REPO_ROOT/pass" && ./build.sh "$cmd")

echo "Building sleep..."
(cd "$REPO_ROOT/sleep" && ./build.sh "$cmd")
;;
install)
echo "Install to $PREFIX"
cp $REPO_ROOT/shell/pass$EXT $PREFIX/bin
cp $REPO_ROOT/shell/gitprompt$EXT $PREFIX/bin
cp $REPO_ROOT/shell/sleep$EXT $PREFIX/bin
;;
*)
echo "Usage: $0 [build|build-all|clean|update-deps]" >&2
Expand Down
15 changes: 15 additions & 0 deletions sleep/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
OUTDIR := ../shell
CC ?= gcc
CFLAGS ?= -O2 -Wall -Wextra
LDFLAGS ?= -s

.PHONY: build build-all clean

build:
$(CC) $(CFLAGS) $(LDFLAGS) -o $(OUTDIR)/sleep sleep.c

# Cross-compilation is not wired up for the C tool; native only.
build-all: build

clean:
rm -f $(OUTDIR)/sleep $(OUTDIR)/sleep.exe
46 changes: 46 additions & 0 deletions sleep/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/sh
# Build helper for the sleep tool (C, no Go toolchain required).
# Equivalent to the targets in ./Makefile.
#
# Usage:
# ./build.sh # native build → ../shell/sleep[.exe]
# ./build.sh build # same
# ./build.sh clean
#
# Requires a C compiler (cc/gcc/clang). Override with CC=...

set -eu

OUTDIR=../shell
CC=${CC:-gcc}
CFLAGS=${CFLAGS:--O2 -Wall -Wextra}
LDFLAGS=${LDFLAGS:--s}

# Resolves to ".exe" on Windows, "" elsewhere.
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*|*NT*) EXT=.exe ;;
*) EXT= ;;
esac

cmd=${1:-build}

case "$cmd" in
build)
mkdir -p "$OUTDIR"
$CC $CFLAGS $LDFLAGS -o "$OUTDIR/sleep$EXT" sleep.c
;;
clean)
rm -f "$OUTDIR/sleep" "$OUTDIR/sleep.exe"
;;
update-deps)
# No dependencies; present so ./shell/make.sh can pass the target through.
;;
build-all)
# Cross-compilation is not wired up for the C tool; native only.
"$0" build
;;
*)
echo "Usage: $0 [build|build-all|update-deps|clean]" >&2
exit 1
;;
esac
97 changes: 97 additions & 0 deletions sleep/sleep.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* sleep - minimal GNU-sleep-alike, primarily for Windows.
*
* Usage: sleep NUMBER[smhd]...
* Sleeps for the sum of all arguments. NUMBER may be fractional.
* Suffixes: s = seconds (default), m = minutes, h = hours, d = days.
*
* Rationale: Windows ships no `sleep`. `timeout /t N` is integer-only and
* fails with "Input redirection is not supported" when stdin is not a
* console, which is exactly the case when invoked from goal's `run` (or any
* other process that pipes stdin). This provides a real executable on %PATH%
* so `run"sleep""0.25"` works.
*
* Build: see ./build.sh (or ./Makefile).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef _WIN32
#include <windows.h>
#else
#include <errno.h>
#include <time.h>
#endif

static void usage(void) {
fputs("usage: sleep NUMBER[smhd]...\n", stderr);
}

/* Sleep for `seconds`, chunked so long waits cannot overflow a DWORD of ms. */
static void sleep_seconds(double seconds) {
while (seconds > 0.0) {
double chunk = seconds > 1000.0 ? 1000.0 : seconds;
#ifdef _WIN32
Sleep((DWORD)(chunk * 1000.0 + 0.5));
#else
struct timespec ts;
ts.tv_sec = (time_t)chunk;
ts.tv_nsec = (long)((chunk - (double)ts.tv_sec) * 1e9);
while (nanosleep(&ts, &ts) == -1 && errno == EINTR) {
/* resume remaining time */
}
#endif
seconds -= chunk;
}
}

/* Parse one interval argument. Returns 0 on success, -1 on bad input. */
static int parse_interval(const char *arg, double *out) {
char *end = NULL;
double v = strtod(arg, &end);

if (end == arg) {
return -1;
}
switch (*end) {
case '\0': break;
case 's': end++; break;
case 'm': v *= 60.0; end++; break;
case 'h': v *= 3600.0; end++; break;
case 'd': v *= 86400.0; end++; break;
default:
return -1;
}
if (*end != '\0' || v < 0.0) {
return -1;
}
*out = v;
return 0;
}

int main(int argc, char **argv) {
double total = 0.0;
int i;

if (argc < 2) {
usage();
return 2;
}

for (i = 1; i < argc; i++) {
double v;

if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
usage();
return 0;
}
if (parse_interval(argv[i], &v) != 0) {
fprintf(stderr, "sleep: invalid time interval '%s'\n", argv[i]);
return 1;
}
total += v;
}

sleep_seconds(total);
return 0;
}
103 changes: 103 additions & 0 deletions specs/009-sleep-command/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Windows `sleep` Command Specification

## Overview

This specification describes a small C utility, `sleep`, that provides a
GNU-`sleep`-compatible subset on Windows. Its primary consumer is the
[goal](https://codeberg.org/anaseto/goal) interpreter's `run` builtin, which
cannot delay execution because Windows ships no `sleep` executable.

## Status

- **Status**: Implemented ✅
- **Created**: 2026-08-09
- **Branch**: `feat/009-sleep-command`

## Background

In goal, shell-outs look like:

```k
run"curl""-V"
```

`run` execs the named program directly (it does not go through `cmd.exe`,
although Go's `LookPath` semantics mean `PATHEXT` entries such as `.cmd` are
resolved). On Windows there is no `sleep` on `%PATH%`, so:

```k
run"sleep""2"
```

fails with a "not available on %PATH%" style error.

## Problem Statement

The obvious Windows substitutes are all inadequate:

| Candidate | Problem |
|---|---|
| `timeout /t N` | Integer seconds only, and aborts with `ERROR: Input redirection is not supported` when stdin is not a console — precisely the case under `run`. |
| `ping -n N 127.0.0.1` | Integer seconds only; abuses the network stack; off-by-one semantics. |
| `powershell Start-Sleep` | ~200-400 ms of interpreter startup overhead per call. |
| `.cmd` shim | Resolvable via `PATHEXT`, but only wraps one of the above. |

## Goals

- Provide a real `sleep.exe` on `%PATH%` usable from goal's `run`.
- Support fractional seconds (`sleep 0.25`).
- Support the GNU suffixes `s`, `m`, `h`, `d`.
- Sum multiple arguments, as GNU `sleep` does (`sleep 1m 30`).
- Negligible startup overhead; no runtime dependencies.

## Non-Goals

- Full GNU coreutils compatibility (no `--version`, no locale handling).
- Cross-compilation. Unix platforms already ship `sleep`; the POSIX code path
exists only so the source builds and can be tested anywhere.

## Design

Single translation unit, `sleep/sleep.c`:

- `parse_interval()` — `strtod` plus an optional single-character suffix.
Rejects trailing garbage and negative values.
- `sleep_seconds()` — `Sleep()` on Windows, `nanosleep()` (with `EINTR`
resume) elsewhere. Chunked at 1000 s so long waits cannot overflow the
`DWORD` millisecond argument.
- Arguments are summed before sleeping.

### Interface

```
usage: sleep NUMBER[smhd]...
```

| Exit code | Meaning |
|---|---|
| 0 | Slept successfully, or `--help`/`-h` |
| 1 | Invalid time interval |
| 2 | No arguments given |

Errors are written to stderr as `sleep: invalid time interval '<arg>'`.

## Build & Install

Follows the existing per-tool convention (`gitprompt`, `pass`): a `build.sh`
and `Makefile` that emit into `../shell/`.

```sh
cd sleep && ./build.sh # → ../shell/sleep[.exe]
./shell/make.sh # builds all tools, including sleep
```

Install by copying `shell/sleep.exe` to a directory on `%PATH%`.

## Acceptance Criteria

- [x] `goal -e 'say run"sleep""2"'` waits ~2 s and exits cleanly.
- [x] `goal -e 'run"sleep""0.25"'` honours fractional seconds.
- [x] `sleep 1m 30` sums to 90 s.
- [x] `sleep abc` prints an error and exits 1.
- [x] `sleep` with no arguments prints usage and exits 2.
- [x] `./shell/make.sh clean` removes the built binary.
Loading