Skip to content

Latest commit

 

History

History
641 lines (510 loc) · 21.9 KB

File metadata and controls

641 lines (510 loc) · 21.9 KB

cclsh guide

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.

Installation

See Installing cclsh for Nix, source builds and login-shell setup.

How a line is interpreted

On Enter, cclsh classifies the line:

  1. A line starting with ( (after whitespace) is read and evaluated as Lisp in cclsh-user. Values print one per line.
  2. 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 command instance runs as a builtin
    • otherwise the word is looked up in PATH
  3. 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.

  4. 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* prints HI. The highlighter shows such words magenta.
  5. 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.

Command mode

Words split on whitespace with quoting and expansion:

SyntaxMeaning
~, ~/xhome directory, at the start of a bare word
$VARenvironment 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
\xescape: \ = joins words, =\* is a literal star
  • Adjacent pieces concatenate into one argument: pre"mid dle"post is premid dlepost.
  • Dotfiles match globs that include the dot: .h* finds .hidden.
  • An unmatched glob stays literal: echo *.zip prints *.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.

Lisp substitution

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. NIL vanishes, like an unset $VAR.
  • Inside a larger word the value concatenates: x(+ 1 2)y is x3y. NIL contributes 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.

Lisp mode

(+ 1 2)                     ; prints 3
(defvar my-projects "~/common-lisp")
(ccl:getenv "HOME")
  • Evaluation is in cclsh-user, which uses cl, ccl and cclsh.
  • *, ** 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) says undefined 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.

Environment variables

export EDITOR=vim PAGER=less
export PATH=$HOME/bin:$PATH
export EDITOR
export
unset PAGER

From 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_proxy need strings.
  • In command mode, ~ expands only at the start of a bare word. Use $HOME in assignment values.
  • In cclsh-user, export is the shell command. Use cl:export for the package operator.
  • CCLSH_PACKAGE holds the current Lisp package name. cclsh refreshes it before each prompt and in each command environment.

Pipelines

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")))
  • glob applies 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, -l is a Lisp variable.
  • cmd is 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.
  • run is the function counterpart when the program name is computed. It uses ordinary function-call semantics. Spread a computed list with apply:
(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.

Unicode

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.

Job control

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 1

The 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:

  • %1 or 1 by id
  • %+, %% or === for the current job
  • %- for the previous job
  • %text for the most recent job whose command starts with text

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.
  • exit and 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.

Defining commands

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 &optional parameters are positional
  • &rest is repeating
  • &key parameters 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, :path or :pathname, :directory, :command, :package, :environment-variable, :job, :choice, or a conversion function
  • :short and :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 -, updates PWD and OLDPWD
  • exit
  • export
  • unset
  • rehash: drop cached PATH lookups and completions
  • commands: list all defined commands
  • help: the built-in manual; help SECTION elaborates
  • edit
  • jobs, fg, bg, disown
  • zoxide-setup

Directory change hooks

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.

zoxide

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:

CommandAction
zgo home
z -go to OLDPWD
z ./existingenter an existing path
z project srcquery zoxide and enter the best match
zi projectselect a zoxide match through fzf

Editing recorded definitions

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.

Line editing and completion

The built-in prompt shows =username@hostname (PACKAGE) directory $. See *Configuration to replace it.

KeyAction
Tabcomplete commands, files or symbols
Left, C-bmove cursor left
Right, C-fmove right or accept a suggestion
Ctrl-Left/Rightmove backward/forward by word
Up/Down, C-p/C-nolder/newer matching history entry
Home/End, C-a/C-estart/end of line
Backspacedelete backward
Deletedelete forward
C-d on textdelete forward
C-d on empty lineexit the shell
C-w, C-Backspace/C-hdelete word backward
C-kkill to end of line
C-udiscard the whole line
C-lclear screen
C-cabort the current line
Alt/Ctrl-Enterinsert a newline
Shift-Entersame, 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=value and -- 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

History

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.

Configuration

~/.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.lisp

The 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)

Scripting

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' | cclsh

The built-in manual is also a stateless command-line action:

cclsh help
cclsh help scripting
cclsh help cd

Use 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.