diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go
index 1688d2d..11bd913 100644
--- a/cmd/cmsh/main.go
+++ b/cmd/cmsh/main.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+	"fmt"
 	"github.com/chzyer/readline"
 	"log"
 	"ucl.lmika.dev/repl"
@@ -22,13 +23,24 @@ func main() {
 	)
 	ctx := context.Background()
 
+	instRepl.SetCommand("hello", func(ctx context.Context, args ucl.CallArgs) (any, error) {
+		fmt.Println("hello")
+		return nil, nil
+	}, repl.Doc{
+		Brief: "displays hello",
+		Detailed: `
+			This displays the message 'hello' to the terminal.
+			It then terminates.
+		`,
+	})
+
 	for {
 		line, err := rl.Readline()
 		if err != nil { // io.EOF
 			break
 		}
 
-		if err := ucl.EvalAndDisplay(ctx, inst, line); err != nil {
+		if err := instRepl.EvalAndDisplay(ctx, line); err != nil {
 			log.Printf("%T: %v", err, err)
 		}
 	}
diff --git a/repl/docs.go b/repl/docs.go
index a8ba0e4..b3ea160 100644
--- a/repl/docs.go
+++ b/repl/docs.go
@@ -1,6 +1,93 @@
 package repl
 
+import (
+	"context"
+	"errors"
+	"fmt"
+	"github.com/lmika/gopkgs/fp/maps"
+	"os"
+	"sort"
+	"strings"
+	"text/tabwriter"
+	"ucl.lmika.dev/ucl"
+	"unicode"
+)
+
 type Doc struct {
 	Brief    string
 	Detailed string
 }
+
+func (d Doc) config(cmdName string, r *REPL) {
+	r.commandDocs[cmdName] = d
+}
+
+func (r *REPL) helpBuiltin(ctx context.Context, args ucl.CallArgs) (any, error) {
+	switch {
+	case args.NArgs() == 0:
+		names := maps.Keys(r.commandDocs)
+		sort.Strings(names)
+
+		tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
+
+		for _, name := range names {
+			cdoc := r.commandDocs[name]
+			if cdoc.Brief != "" {
+				fmt.Fprintf(tabWriter, "%v\t  %v\n", name, r.commandDocs[name].Brief)
+			} else {
+				fmt.Fprintf(tabWriter, "%v\n", name)
+			}
+		}
+
+		tabWriter.Flush()
+	default:
+		var cmdName string
+		if err := args.Bind(&cmdName); err != nil {
+			return nil, err
+		}
+
+		docs, ok := r.commandDocs[cmdName]
+		if !ok {
+			return nil, errors.New("no help docs found for command")
+		}
+
+		fmt.Printf("%v\n", cmdName)
+		fmt.Printf("  %v\n", docs.Brief)
+
+		if docs.Detailed != "" {
+			fmt.Println("")
+			fmt.Println("Details:")
+
+			lines := strings.Split(docs.Detailed, "\n")
+
+			trimLeft := 0
+			if len(lines) == 0 {
+				return nil, nil
+			} else if len(strings.TrimSpace(lines[0])) == 0 && len(lines) > 1 {
+				// indicates that the next line should indicate the indentation
+				trimLeft = len(lines[1]) - len(strings.TrimLeftFunc(lines[1], unicode.IsSpace))
+				lines = lines[1:]
+			}
+
+			for _, line := range lines {
+				fmt.Printf("  %v\n", trimSpaceLeftUpto(line, trimLeft))
+			}
+		}
+	}
+	return NoResults{}, nil
+}
+
+func trimSpaceLeftUpto(s string, n int) string {
+	if n == 0 {
+		return s
+	}
+
+	for i, c := range s {
+		if i >= n {
+			return s[i:]
+		} else if !unicode.IsSpace(c) {
+			return s[i:]
+		}
+	}
+	return s
+}
diff --git a/repl/evaldisplay.go b/repl/evaldisplay.go
index c8704ec..8b1abb5 100644
--- a/repl/evaldisplay.go
+++ b/repl/evaldisplay.go
@@ -7,6 +7,8 @@ import (
 	"ucl.lmika.dev/ucl"
 )
 
+type NoResults struct{}
+
 func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error {
 	res, err := r.inst.Eval(ctx, expr)
 	if err != nil {
@@ -18,20 +20,26 @@ func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error {
 
 func displayResult(ctx context.Context, inst *ucl.Inst, res any) (err error) {
 	switch v := res.(type) {
+	case NoResults:
+		return nil
 	case nil:
 		if _, err = fmt.Fprintln(os.Stdout, "(nil)"); err != nil {
 			return err
 		}
-	case listable:
+	case ucl.Listable:
 		for i := 0; i < v.Len(); i++ {
 			if err = displayResult(ctx, inst, v.Index(i)); err != nil {
 				return err
 			}
 		}
-	default:
+	case ucl.Object:
 		if _, err = fmt.Fprintln(os.Stdout, v.String()); err != nil {
 			return err
 		}
+	default:
+		if _, err = fmt.Fprintln(os.Stdout, v); err != nil {
+			return err
+		}
 	}
 	return nil
 }
diff --git a/repl/repl.go b/repl/repl.go
index ec872c1..387354d 100644
--- a/repl/repl.go
+++ b/repl/repl.go
@@ -1,10 +1,6 @@
 package repl
 
 import (
-	"context"
-	"fmt"
-	"github.com/lmika/gopkgs/fp/maps"
-	"sort"
 	"ucl.lmika.dev/ucl"
 )
 
@@ -25,7 +21,16 @@ func New(opts ...ucl.InstOption) *REPL {
 		inst:        inst,
 		commandDocs: make(map[string]Doc),
 	}
-	inst.SetBuiltin("help", r.helpBuiltin)
+	r.SetCommand("help", r.helpBuiltin, Doc{
+		Brief: "displays help about a command",
+		Detailed: `
+			When used without arguments, 'help' will display the list of known commands,
+			along with a brief description on what each one does.
+
+			When used with an argument, 'help' will display a more detailed explanation
+			of what each command does.
+		`,
+	})
 
 	return r
 }
@@ -42,23 +47,3 @@ func (r *REPL) SetCommand(name string, fn ucl.BuiltinHandler, opts ...CommandOpt
 
 	r.inst.SetBuiltin(name, fn)
 }
-
-func (r *REPL) helpBuiltin(ctx context.Context, args ucl.CallArgs) (any, error) {
-	switch {
-	case args.NArgs() == 0:
-		// TEMP
-		names := maps.Keys(r.commandDocs)
-		sort.Strings(names)
-
-		for _, name := range names {
-			cdoc := r.commandDocs[name]
-			if cdoc.Brief != "" {
-				fmt.Println("%v\t%v", name, r.commandDocs[name])
-			} else {
-				fmt.Println("%v", name)
-			}
-		}
-		// END TEMP
-	}
-	return nil, nil
-}