commit af271b39941c887cdaa27b865329f29757f84b1b
parent c516781ad1ac2b3a3d54f312ebd19d223bcedb7b
Author: Robert Russell <robert@rr3.xyz>
Date: Sun, 26 Jul 2026 18:58:36 -0700
Create man page
Diffstat:
5 files changed, 181 insertions(+), 11 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+example-trace.txt
+graft.1
diff --git a/Makefile b/Makefile
@@ -2,15 +2,33 @@
PREFIX = /usr/local
BINDIR = $(PREFIX)/bin
+MANDIR = $(PREFIX)/share/man
+MAN1DIR = $(MANDIR)/man1
-all:
+all: README graft.1
-install:
+README: graft.1
+ mandoc -O width=80 ./graft.1 | col -bx > README
+
+graft.1: graft.1.template example-trace.txt
+ sed -e '/^{example-trace.txt}$$/{r example-trace.txt' -e 'd}' < graft.1.template > graft.1
+
+example-trace.txt: graft gen-example-trace.sh
+ ./gen-example-trace.sh > example-trace.txt
+
+install: graft graft.1
mkdir -p $(DESTDIR)$(BINDIR)
cp -f graft $(DESTDIR)$(BINDIR)/graft
chmod 755 $(DESTDIR)$(BINDIR)/graft
+ mkdir -p $(DESTDIR)$(MAN1DIR)
+ cp -f graft.1 $(DESTDIR)$(MAN1DIR)/graft.1
+ chmod 644 $(DESTDIR)$(MAN1DIR)/graft.1
uninstall:
rm -f $(DESTDIR)$(BINDIR)/graft
+ rm -f $(DESTDIR)$(MAN1DIR)/graft.1
+
+clean:
+ rm -f README graft.1 example-trace.txt
-.PHONY: noop install uninstall
+.PHONY: all install uninstall clean
diff --git a/README b/README
@@ -1,11 +1,117 @@
-Graft: a simple "package manager"
+GRAFT(1) User Commands GRAFT(1)
-Graft does not handle remote fetching, build steps, versions, dependencies,
-package removal, or most other features you would expect from a "package
-manager" (hence the scare quotes). Graft merely links or copies
-files/directories into place and then prints what happened.
+NAME
+ graft - simple "package manager"
-You specify package installation steps in a `.graft.sh` script at the top-level
-of the package. See `example-pkg`.
+SYNOPSIS
+ graft [-hn] ROOT PKGS...
-Graft is mainly intended for configuration file ("dotfile") management.
+DESCRIPTION
+ graft is a simple "package manager" that merely links or copies
+ files/directories into place and then prints what happened.
+
+ graft does not handle remote fetching, build steps, versions,
+ dependencies, package removal, or most other features you would expect
+ from a "package manager" (hence the scare quotes).
+
+OPTIONS
+ -h Display a help message.
+
+ -n Enable dry-run mode, causing graft to not write any files but
+ still print-out what it would do. (More precisely, this only
+ affects the link and copy functions. .graft.sh scripts can still
+ do anything.)
+
+NOTES
+ graft is mainly intended for configuration file ("dotfile") management.
+
+EXAMPLE
+ $ tree -a --noreport example-pkg
+ example-pkg
+ ├── .graft.sh
+ ├── a
+ │ ├── 0.txt
+ │ ├── 1.txt
+ │ └── 2.txt
+ ├── b
+ │ ├── 0.txt
+ │ ├── 1.txt
+ │ └── 2.txt
+ ├── bar.txt
+ └── foo.txt
+
+ $ cat example-pkg/.graft.sh
+ # Print messages during install:
+ msg_note "This is an important note (e.g., make sure you manually do this thing)"
+ msg_info "This is information"
+ msg_warning "This is a warning"
+ msg_error "This is an error (not fatal)"
+ # msg_fatal "This is an error (fatal)"
+
+ # Link files/directories:
+ link foobar/foo.txt foo.txt
+ link a-link a
+ for f in b/*; do
+ link b-links/"$(basename "$f")" "$f"
+ done
+
+ # Copy files/directories:
+ copy foobar/bar.txt bar.txt
+ copy a-copy a
+ for f in b/*; do
+ copy b-copies/"$(basename "$f")" "$f"
+ done
+
+ $ graft example-root example-pkg
+ [example-pkg] NOTE: This is an important note (e.g., make sure you manually do this thing)
+ [example-pkg] INFO: This is information
+ [example-pkg] WARNING: This is a warning
+ [example-pkg] ERROR: This is an error (not fatal)
+ [example-pkg] LINK CREATE: /<omitted>/example-root/foobar/foo.txt --> foo.txt
+ [example-pkg] LINK CREATE: /<omitted>/example-root/a-link --> a
+ [example-pkg] LINK CREATE: /<omitted>/example-root/b-links/0.txt --> b/0.txt
+ [example-pkg] LINK CREATE: /<omitted>/example-root/b-links/1.txt --> b/1.txt
+ [example-pkg] LINK CREATE: /<omitted>/example-root/b-links/2.txt --> b/2.txt
+ [example-pkg] COPY CREATE: /<omitted>/example-root/foobar/bar.txt <-- bar.txt
+ [example-pkg] COPY CREATE: /<omitted>/example-root/a-copy <-- a
+ [example-pkg] COPY CREATE: /<omitted>/example-root/b-copies/0.txt <-- b/0.txt
+ [example-pkg] COPY CREATE: /<omitted>/example-root/b-copies/1.txt <-- b/1.txt
+ [example-pkg] COPY CREATE: /<omitted>/example-root/b-copies/2.txt <-- b/2.txt
+
+ $ tree -a --noreport example-root
+ example-root
+ ├── a-copy
+ │ ├── 0.txt
+ │ ├── 1.txt
+ │ └── 2.txt
+ ├── a-link -> /<omitted>/example-pkg/a
+ ├── b-copies
+ │ ├── 0.txt
+ │ ├── 1.txt
+ │ └── 2.txt
+ ├── b-links
+ │ ├── 0.txt -> /<omitted>/example-pkg/b/0.txt
+ │ ├── 1.txt -> /<omitted>/example-pkg/b/1.txt
+ │ └── 2.txt -> /<omitted>/example-pkg/b/2.txt
+ └── foobar
+ ├── bar.txt
+ └── foo.txt -> /<omitted>/example-pkg/foo.txt
+
+ $ graft example-root example-pkg # Idempotent
+ [example-pkg] NOTE: This is an important note (e.g., make sure you manually do this thing)
+ [example-pkg] INFO: This is information
+ [example-pkg] WARNING: This is a warning
+ [example-pkg] ERROR: This is an error (not fatal)
+ [example-pkg] LINK OK: /<omitted>/example-root/foobar/foo.txt --> foo.txt
+ [example-pkg] LINK OK: /<omitted>/example-root/a-link --> a
+ [example-pkg] LINK OK: /<omitted>/example-root/b-links/0.txt --> b/0.txt
+ [example-pkg] LINK OK: /<omitted>/example-root/b-links/1.txt --> b/1.txt
+ [example-pkg] LINK OK: /<omitted>/example-root/b-links/2.txt --> b/2.txt
+ [example-pkg] COPY OK: /<omitted>/example-root/foobar/bar.txt <-- bar.txt
+ [example-pkg] COPY OK: /<omitted>/example-root/a-copy <-- a
+ [example-pkg] COPY OK: /<omitted>/example-root/b-copies/0.txt <-- b/0.txt
+ [example-pkg] COPY OK: /<omitted>/example-root/b-copies/1.txt <-- b/1.txt
+ [example-pkg] COPY OK: /<omitted>/example-root/b-copies/2.txt <-- b/2.txt
+
+
+graft 2026-07-26 GRAFT(1)
diff --git a/gen-example-trace.sh b/gen-example-trace.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+set -euo pipefail
+
+run() {
+ printf '$ %s\n' "$1"
+ eval "$1" 2>&1 | sed -E 's:/.*/example-(root|pkg):/<omitted>/example-\1:'
+ printf '\n'
+}
+
+mkdir -p example-root
+trap 'rm -rf example-root' EXIT
+
+run 'tree -a --noreport example-pkg'
+run 'cat example-pkg/.graft.sh'
+PATH=".:$PATH" run 'graft example-root example-pkg'
+run 'tree -a --noreport example-root'
+PATH=".:$PATH" run 'graft example-root example-pkg # Idempotent'
diff --git a/graft.1.template b/graft.1.template
@@ -0,0 +1,27 @@
+.TH "GRAFT" "1" "2026-07-26" "graft" "User Commands"
+.SH NAME
+graft \- simple "package manager"
+.SH SYNOPSIS
+\fBgraft\fR [\-hn] \fIROOT\fR \fIPKGS\fR...
+.SH DESCRIPTION
+\fBgraft\fR is a simple "package manager" that merely links or copies
+files/directories into place and then prints what happened.
+.P
+\fBgraft\fR does not handle remote fetching, build steps, versions,
+dependencies, package removal, or most other features you would expect
+from a "package manager" (hence the scare quotes).
+.SH OPTIONS
+.TP
+\fB\-h\fR
+Display a help message.
+.TP
+\fB\-n\fR
+Enable dry-run mode, causing \fBgraft\fR to not write any files but still
+print-out what it would do. (More precisely, this only affects the \fIlink\fR
+and \fIcopy\fR functions. \fI.graft.sh\fR scripts can still do anything.)
+.SH NOTES
+\fBgraft\fR is mainly intended for configuration file ("dotfile") management.
+.SH EXAMPLE
+.EX
+{example-trace.txt}
+.EE