cclsh is a system shell in a live Clozure CL image. Unix commands and Common Lisp share one process. Pipelines and redirection are Lisp forms.
The built-in manual is available as help inside cclsh, or as cclsh help
from another shell. help SECTION opens a topic.
See Installing cclsh for Nix, source builds and login-shell setup.
On Enter, cclsh classifies the line:
- A line starting with
((after whitespace) is read and evaluated as Lisp incclsh-user. Values print one per line. - Otherwise it is a command line. The first word resolves in order:
- an existing non-directory word containing
/runs as a path - a symbol in the current package bound to a
commandinstance runs as a builtin - otherwise the word is looked up in
PATH
- an existing non-directory word containing
- If that misses and the expanded line is one explicit directory path,
cclsh changes into it. Implicit cd accepts:
..- absolute paths
- paths beginning
./or../ - expanded
~/...paths - names ending in
/
The path is the only word. Builtins and executables win first. Command-position Tab completion adds the trailing slash.
- If that misses and the line is a single word naming a bound variable
or keyword, or a number literal, it evaluates REPL-style. After
(defvar *balls* 'hi),*balls*printsHI. The highlighter shows such words magenta. - Otherwise: red
command not found, status 127.
Only symbols bound to command instances are builtins. time runs
/bin/time.
PATH lookups are cached for highlighting. Execution retries a cached
miss, so a freshly installed program runs immediately. rehash drops
the caches.
Words split on whitespace with quoting and expansion:
| Syntax | Meaning |
|---|---|
~, ~/x | home directory, at the start of a bare word |
$VAR | environment variable, empty when unset |
${VAR} | same, with explicit braces |
*, ? | glob against the filesystem, per path segment |
"..." | one word, $VAR expands inside |
'...' | one word, everything literal |
(form) | Lisp substitution; $(form) is the same |
\x | escape: \ = joins words, =\* is a literal star |
- Adjacent pieces concatenate into one argument:
pre"mid dle"postispremid dlepost. - Dotfiles match globs that include the dot:
.h*finds.hidden. - An unmatched glob stays literal:
echo *.zipprints*.zip. - Globs apply to whole bare words.
- A leading
;comments out the submitted line. The previous status is kept. Elsewhere,;is a literal argument. Inside Lisp forms, semicolons are Common Lisp comments. Comment lines go into history. #is a literal argument.- Pipelines and redirection are Lisp forms. See *Pipelines.
Parentheses inside a command line evaluate as Lisp and substitute the value. The outer parens are shell delimiters: one form inside is an expression, several forms become one function call.
echo (*balls*)
echo (+ 1 2)
echo (string-downcase *balls*)
mv draft.txt (format nil "post-~a.txt" (get-universal-time))$(form)is an alternative spelling.- A standalone substitution returning a proper list splices into several
arguments.
NILvanishes, like an unset$VAR. - Inside a larger word the value concatenates:
x(+ 1 2)yisx3y.NILcontributes an empty string. - Substitution results are used verbatim.
- Quote parens to keep them literal:
'(1 2 3)'stays text. - An unterminated substitution continues on the next line. Tab completes Lisp symbols inside the parens.
(+ 1 2) ; prints 3
(defvar my-projects "~/common-lisp")
(ccl:getenv "HOME")- Evaluation is in
cclsh-user, which usescl,cclandcclsh. *,**and***hold the last three primary values.cclsh:*last-status*holds the last command’s exit status.- Unfinished input continues under a
...prompt: unbalanced Lisp forms, open strings, and a command line ending in a backslash. History recalls multi-line entries with their original newlines. - An undefined function is reported with close suggestions:
(defparam *x* 1)saysundefined function defparam, did you mean defparameter?. The call is rejected before its arguments run. - Errors print in red and return you to the prompt.
When a form’s output omits a trailing newline, cclsh prints a reverse
video ⏎ marker before the value: (format t "hello") shows hello⏎
and then NIL. The prompt applies the same rule after commands like
printf hi.
export EDITOR=vim PAGER=less
export PATH=$HOME/bin:$PATH
export EDITOR
export
unset PAGERFrom Lisp, names are designators: symbols, keywords and strings all work. Non-string values are stringified:
(setenv 'editor "vim") ; reader upcases: sets EDITOR
(getenv :home) ; "/home/mag"
(setf (env 'port) 8000) ; ENV is a setf-able accessor
(env 'port) ; "8000"
(unset 'port)
(environment-variables) ; live list of NAME=value strings- Lowercase names like
http_proxyneed strings. - In command mode,
~expands only at the start of a bare word. Use$HOMEin assignment values. - In
cclsh-user,exportis the shell command. Usecl:exportfor the package operator. CCLSH_PACKAGEholds the current Lisp package name. cclsh refreshes it before each prompt and in each command environment.
Process orchestration is Lisp. Stage heads resolve like the first word
of a command line. Stage arguments are evaluated expressions: a scalar
becomes one stringified argument, NIL vanishes, and a
proper list splices one level.
(cmd git "status")
(pipe (ls "-la") (grep "lisp"))
(pipe (ls "-1" (glob "screenshot-*"))
(wc "-l"))
(seq (make "clean") (make))
(all (make) (make "install"))
(any (probe) (echo "fallback"))
(let ((pattern "defun"))
(pipe (git "grep" pattern) (wc "-l")))globapplies the same~, environment-variable,*and?expansion as a bare command word and returns a proper list. Matches are sorted within each pattern; pattern order is kept. An unmatched pattern stays literal. Ordinary strings are literal:"-l"is a flag,-lis a Lisp variable.cmdis the single-command sibling of a pipe stage. The head resolves like a command word; the arguments are evaluated Lisp expressions. The exit status comes back.runis the function counterpart when the program name is computed. It uses ordinary function-call semantics. Spread a computed list withapply:
(apply #'run "ls" "-1" (glob "screenshot-*"))Redirection is spelled as stages inside pipe and capture:
(from "file")first, feeds standard input(to "file")or(append-to "file")last, writes standard output(error-to "file")or(error-append-to "file")sends every stage’s standard error to a file(merge-error)sends standard error onto standard output
capture returns the output as a string with trailing newlines
trimmed.
(pipe (make) (to "build.log"))
(pipe (make "install") (append-to "build.log"))
(pipe (from "access.log") (grep "500") (wc "-l"))
(pipe (from "a.bin") (to "b.bin"))
(pipe (make) (error-to "errors.log"))
(pipe (make) (to "all.log") (merge-error))
(capture (make) (merge-error))
(capture (git "rev-parse" "HEAD"))
(defvar *head* (capture (git "rev-parse" "--short" "HEAD")))From the command line:
echo deployed (capture (git "rev-parse" "--short" "HEAD"))File redirection is byte-transparent. capture decodes UTF-8. Error
redirection covers builtin stages’ *error-output* too.
Each form returns the deciding exit status and records it in
*last-status*. Builtin commands can appear inside pipe; they run
concurrently and stream to the next stage. A synchronous run inside
such a builtin joins the pipeline.
cclsh uses UTF-8 for terminal I/O, startup and history files, scripts, prompt output, environment names and values, child arguments, executable paths, and captured pipeline text.
The editor measures terminal cells. Wide CJK and emoji glyphs wrap correctly. Combining marks and joined emoji move and delete as one grapheme.
Redirect-only pipelines are byte-transparent. (from file) and
(to file) copy arbitrary binary data.
A trailing & launches an external command as a background job.
Ctrl-Z stops the foreground command or pipeline. jobs, fg, bg and
disown manage the job table:
sleep 300 &
make build
jobs
bg %2
fg
disown 1The first command prints its job id and process id. Ctrl-Z during
make build creates the stopped second job. jobs marks the current
job with +. bg %2 continues it in the background. fg brings the
current job back to the terminal. disown 1 drops the first job from
the table; the process keeps running.
Job specs:
%1or1by id%+,%%or === for the current job%-for the previous job%textfor the most recent job whose command starts withtext
Omitting the argument, fg, bg and disown act on the current job.
disown also accepts a command substring: disown build. jobs -l
adds process group ids, which is what kill wants: kill -TERM -12345
ends a whole job. The builtins are ordinary functions: (fg 1),
(disown "build"), (jobs).
- Finished background jobs are announced before the next prompt as
Done,Exit 2, or the terminating signal. - A stopped job restores its terminal modes under
fg. - Every external stage in a pipeline shares one process group. Builtin workers stop, resume and terminate with that group.
exitand Ctrl-D warn once if stopped jobs exist. A second exit proceeds. Orderly exit sends SIGHUP to tracked live job groups, then SIGCONT to stopped groups.
defcommand creates a command callable from the command line and from
Lisp as an ordinary function:
(defcommand gs ()
"Short git status."
(run "git" "status" "--short"))
(defcommand mkcd (directory)
"Create DIRECTORY and change into it."
(:arguments
(directory :type :directory :help "Directory to create."))
(run "mkdir" "-p" directory)
(cd directory))After that, gs works at the prompt, and so do (gs) and
(mkcd "/tmp/scratch"). An integer return value becomes the exit
status; any other value means 0. run executes one program in the
foreground and returns its exit status. Arguments are stringified with
princ-to-string.
An :arguments declaration after the docstring makes the command
self-describing. It follows the Lisp lambda list:
- required and
&optionalparameters are positional &restis repeating&keyparameters are options; the long name defaults to the lowercase parameter name
(defcommand deploy (source &optional (mode "safe")
&key force (jobs 1))
"Deploy SOURCE."
(:arguments
(source :type :directory :help "Source tree to deploy.")
(mode :choices ("safe" "fast") :convert t
:help "Deployment strategy.")
(force :type :boolean :short #\f
:help "Replace an existing deployment.")
(jobs :type :integer :convert t :short #\j :metavariable "COUNT"
:help "Parallel worker count."))
(format t "~a from ~a with ~d worker~:p~%" mode source jobs)
(when force
(format t "replacement enabled~%")))The command line then accepts deploy src fast -f -j4, --jobs 4 and
--jobs=4. Boolean short options can be grouped. -- ends option
parsing. Unknown options, missing values, duplicate non-repeating
options and missing required arguments return status 2. From Lisp, use
ordinary calling conventions:
(deploy "src" "fast" :force t :jobs 4).
Argument properties:
:type::string,:boolean,:integer,:number,:pathor:pathname,:directory,:command,:package,:environment-variable,:job,:choice, or a conversion function:shortand:long: option spellings:metavariable: help text placeholder:help: argument description:required t: require an otherwise optional argument:choices: a list, or a function of(argument context):completion: a function of(argument context)returning candidates, optionally with descriptions:convert t: turn strings into the declared type and validate choices:converter: a custom(argument context)conversion function
Boolean options map to Lisp booleans. With :convert t:
- path kinds become pathnames
- packages and commands resolve to their live objects
- environment-variable names stay strings
- job specifications resolve through the current job table
Every declared command supports COMMAND --help. help COMMAND
renders usage, arguments and options.
A synchronous run inside a defcommand used as a pipe or
capture stage joins that pipeline:
(defcommand ls (&rest arguments)
"List files with exa."
(apply #'run "exa" arguments))
(pipe (ls "-1" (glob "screenshot-*"))
(wc "-l"))The wrapper is one pipeline stage: the child inherits redirection and job control.
Builtins that ship with the shell:
cd: supports-, updatesPWDandOLDPWDexitexportunsetrehash: drop cachedPATHlookups and completionscommands: list all defined commandshelp: the built-in manual;help SECTIONelaborateseditjobs,fg,bg,disownzoxide-setup
Every successful directory change runs the functions registered with
directory-change-hook-add. A hook receives the old and new absolute
directory names after PWD, OLDPWD and CCL’s default directory have
been updated.
(defun announce-directory (old new)
(format t "moved from ~a to ~a~%" old new))
(directory-change-hook-add 'announce-directory)
(directory-change-hook-remove 'announce-directory)Register the function by symbol so redefinition keeps the same hook identity.
Install zoxide, and fzf for interactive selection. Put
(zoxide-setup) in startup.lisp. It records the current directory,
records later changes, and installs two commands:
| Command | Action |
|---|---|
z | go home |
z - | go to OLDPWD |
z ./existing | enter an existing path |
z project src | query zoxide and enter the best match |
zi project | select a zoxide match through fzf |
edit opens a function’s recorded source in a temporary UTF-8 Lisp
file. It uses VISUAL, then EDITOR, then vi. Editor values may
include arguments, such as emacsclient --wait.
edit my-function
edit cl:mapcar
From Lisp:
(edit #'my-function)
(edit 'my-function)After the editor exits, cclsh shows a Colordiff with Lisp syntax
highlighting, evaluates the edited top-level form in the function’s
package, and installs the definition in the live image. Later edit
calls reuse that text.
The built-in prompt shows =username@hostname (PACKAGE) directory $. See *Configuration to replace it.
| Key | Action |
|---|---|
| Tab | complete commands, files or symbols |
| Left, C-b | move cursor left |
| Right, C-f | move right or accept a suggestion |
| Ctrl-Left/Right | move backward/forward by word |
| Up/Down, C-p/C-n | older/newer matching history entry |
| Home/End, C-a/C-e | start/end of line |
| Backspace | delete backward |
| Delete | delete forward |
| C-d on text | delete forward |
| C-d on empty line | exit the shell |
| C-w, C-Backspace/C-h | delete word backward |
| C-k | kill to end of line |
| C-u | discard the whole line |
| C-l | clear screen |
| C-c | abort the current line |
| Alt/Ctrl-Enter | insert a newline |
| Shift-Enter | same, when the terminal reports it |
At a slash-free command position, Tab completes command names and
directories. For commands with :arguments it also completes the
active positional or option value:
- options display with their help
- choices may be static or dynamic
- path, directory, command, package, environment-variable and job arguments use semantic providers
- grouped short options, attached values,
--long=valueand--are honored
Other commands complete files. Lisp symbols complete in Lisp mode,
including :keywords. Directories complete with a trailing /.
Spaces are escaped.
- A unique match inserts itself.
- Several matches extend to their common prefix.
- Tab again opens a candidate grid. Arrow keys navigate; Tab cycles. Escape restores the original prefix.
With text already entered, Up or C-p and Down or C-n traverse history entries containing the draft captured when backward traversal begins. A lowercase draft is case-insensitive. Any uppercase letter makes the search case-sensitive. Down past the newest match restores the draft and cursor. Empty input traverses the complete history.
The dim suggestion is the newest history entry beginning with the current input. Right or C-f at the end of the input accepts it.
Input highlights as you type with the standard 16 terminal colors.
Command mode:
- external commands: green
- builtins and valid implicit directory paths: cyan
- unknown commands: red
- lone bound variables: magenta
- strings: yellow
- numbers: cyan
$VAR: magenta- glob wildcards and a leading
~: bright magenta - backslash escapes: dim
Lisp mode:
- known operators in head position: blue
- keywords and quotes: magenta
- numbers, characters and constants: cyan
- bound
*earmuffed*specials: magenta - strings: yellow
- comments and parens: dim
Interactive sessions load and write ~/.config/cclsh/history
(XDG_CONFIG_HOME is respected), one printed string per entry, capped
at 10000 entries on load. Submitted non-blank lines are recorded.
Immediate duplicates are skipped. Recalled entries keep embedded
newlines.
~/.config/cclsh/startup.lisp loads in cclsh-user for interactive
sessions and configured one-shots. A broken startup file prints its
error and the shell starts. Create it with private permissions:
install -d -m 700 ~/.config/cclsh
touch ~/.config/cclsh/startup.lisp
chmod 600 ~/.config/cclsh/startup.lispThe example startup file is a
host-neutral template: a login-safe PATH, XDG directories, a few
commands and optional zoxide integration.
Prompt rendering is configured through *prompt-function*. The function
receives keyword snapshots:
:status:duration-milliseconds:columns:job-count
A returned string is used verbatim, including ANSI or multiple lines.
nil selects the built-in prompt.
(defun compact-prompt
(&key status job-count &allow-other-keys)
(format nil "[~d/~d] ~a> " status job-count (package-name *package*)))
(setf *prompt-function* 'compact-prompt)Interactive terminal sessions emit OSC 133 semantic shell-integration
markers for prompt start, input start, execution start and command
completion. The completion marker includes the exit status. Set
*semantic-prompt-markers-enabled* to nil in startup.lisp to turn
them off.
Key bindings are configured through *line-editor-keymap*. Events are
semantic names such as :left, :word-right, :kill-line and
:complete. For example, this makes Ctrl-Right move one grapheme:
(clinedi:keymap-bind *line-editor-keymap* :word-right :right)Saved images include Quicklisp:
(ql:quickload :dexador)
(dex:get "https://example.com")quicklisp-setup loads an existing ~/quicklisp or installs one with
curl, useful when running from an unsaved CCL development image.
By convention, scripts are named NAME.sh.lisp. The final .lisp
keeps the file recognizable to Lisp tooling; .sh marks it as
shell-oriented.
Scripts run three ways. They skip startup.lisp and history:
cclsh -c 'echo one shot'
cclsh provision.sh.lisp
printf 'ls\n(+ 1 2)\nexit\n' | cclshThe built-in manual is also a stateless command-line action:
cclsh help
cclsh help scripting
cclsh help cdUse a configured command string when the command needs aliases, PATH
changes or other definitions from startup.lisp:
cclsh -lc 'z project'
cclsh -ic 'echo $EDITOR'The short flags combine in either order: -lc, -cl, -ilc. A
preceding -l or -i also configures a later -c. Configured command
strings skip history. CCLSH_SAFE=1 skips the startup file even in
configured mode.
Script files work as shebang interpreters:
#!/usr/local/bin/cclsh
(format t "deploying~%")
(all (make "build") (make "deploy"))Inside a script, *argv* contains the script path followed by every
argument supplied after it. cclsh provision.sh.lisp alpha "two words"
binds it to ("provision.sh.lisp" "alpha" "two words"). Arguments
beginning with a dash are script data. *argv* is nil outside script
mode. Use cclsh -- -script.sh.lisp argument when the script path
begins with a dash.
The process exit code is the shell’s last status, or the argument of
exit.