{"id":13442513,"url":"https://github.com/ruricolist/cmd","last_synced_at":"2026-02-16T03:34:53.031Z","repository":{"id":49019591,"uuid":"295020341","full_name":"ruricolist/cmd","owner":"ruricolist","description":"Utility for running external programs","archived":false,"fork":false,"pushed_at":"2024-10-27T17:16:17.000Z","size":181,"stargazers_count":64,"open_issues_count":8,"forks_count":5,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-10-27T20:30:49.648Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ruricolist.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2020-09-12T20:23:33.000Z","updated_at":"2024-10-27T17:16:20.000Z","dependencies_parsed_at":"2024-01-31T12:05:01.968Z","dependency_job_id":"ce79e004-13b7-4cc3-a9de-e0fead1f248c","html_url":"https://github.com/ruricolist/cmd","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruricolist%2Fcmd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruricolist%2Fcmd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruricolist%2Fcmd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruricolist%2Fcmd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ruricolist","download_url":"https://codeload.github.com/ruricolist/cmd/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244630118,"owners_count":20484317,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T03:01:46.677Z","updated_at":"2026-02-16T03:34:53.026Z","avatar_url":"https://github.com/ruricolist.png","language":"Common Lisp","readme":"# cmd\n\nA utility for running external programs, built on\n[`uiop:launch-program`][UIOP].\n\nCmd is designed to:\n\n1. Be natural to use.\n2. Protect against shell interpolation.\n3. Be usable from multi-threaded programs.\n4. Support Windows.\n\n## Argument handling\n\nArguments to `cmd` are *never* passed to a shell for interpretation.\n\nArguments are usually:\n- strings\n- keywords\n- lists of strings and keywords\n\nSome other types get special handling. Nested lists are not allowed.\n\nArguments are handled as follows:\n\n1. A string is tokenized (using [cl-shlex][]) and added to the list of\n   arguments.\n\n   ``` lisp\n   (cmd \"ls -al\")\n   ≅ (uiop:wait-process (uiop:launch-program '(\"ls\" \"-al\")))\n\n   (cmd \"echo 'hello world'\")\n   ≅ (uiop:wait-process (uiop:launch-program '(\"echo\" \"hello world\")))\n   ```\n\n   Redirection operators in the tokenized string (such as `\u003c`, `\u003e`, or\n   `|`) are translated into keywords (see below).\n\n   ```lisp\n   (cmd \"echo 'hello world' \u003e myfile\")\n   ≡ (cmd '(\"echo\" \"hello world\" :\u003e \"myfile\"))\n   ```\n\n2. A list is added directly to the list of arguments (not tokenized).\n   (Putting a string in a list is “escaping” it.)\n\n   ``` lisp\n   (cmd \"bash -c 'exit 1'\")\n   ≡ (cmd \"bash -c\" '(\"exit 1\"))\n   ```\n\n   Keywords in the list are treated exactly like keywords as\n   arguments.\n\n3. Keywords that are subcommand dividers (like `|`) are handled\n   internally by `cmd`. Otherwise, a keyword, along with the next\n   value, is used as a keyword argument to UIOP.\n\n   ``` lisp\n   (cmd \"bash -c 'exit 1'\" :ignore-error-status t)\n   ≡ (cmd :ignore-error-status t \"bash -c 'exit 1'\")\n   ≡ (cmd :check nil \"bash -c 'exit 1'\")\n   ≡ (cmd \"bash -c\" :ignore-error-status t '(\"exit 1\"))\n   ≡ (cmd \"bash -c\" :check nil '(\"exit 1\"))\n   ```\n\n   Note that unlike normal Lisp functions, keyword arguments can\n   appear anywhere, not just at the end.\n\n   Also note `:check` is accepted as an alias for\n   `:ignore-error-status`, although the value is negated before being\n   passed to UIOP.\n\n4. Any character, integer, or pathname is directly added to the list\n   of arguments, as if it were an escaped string. (It is an error if a\n   pathname begins with `-`.)\n\n5. Cmd supports a basic form of process substitution, running\n   processes as input to commands expecting files. To construct a\n   process substitution, use the `psub` Lisp function.\n\n   ``` lisp\n   (cmd? \"diff\" (psub \"echo x\") (psub \"echo x\"))\n   =\u003e T\n\n   (cmd? \"diff\" (psub \"echo x\") (psub \"echo y\"))\n   =\u003e NIL\n   ```\n\n   (For this specific case, however – passing a string to a command\n   expecting a file – use `psub-echo` or `psub-format`, which don’t\n   actually call an external program.)\n\n### Parsing the `cmd` DSL\n\nYou can use the `cmd` DSL in your own programs with `cmd:parse-cmd-dsl`. This takes a list of arguments and returns two values: a fully-tokenized list of command arguments and a list of keyword arguments, both suitable for passsing to `uiop:launch-program`.\n\n``` lisp\n(parse-cmd-dsl '(\"echo 'hello world' \u003e myfile\"))\n=\u003e (\"echo\" \"hello world\"), (:OUTPUT \"myfile\")\n```\n\n## The external program’s working directory\n\nCmd is designed with multi-threaded programs in mind. It always runs\nprograms with their working directory relative to\n[`*default-pathname-defaults*`][dpd]. This is because the OS-level\nworking directory of a program, on both Windows and Unix, is the working\ndirectory for the entire process, not the individual thread, and\nchanging it changes it for all threads.\n\nYou can also specify the directory for a particular command with the\nkeyword argument `:in`:\n\n``` lisp\n(cmd \"ls\" :in #p\"/\")\n(cmd :in #p\"/\" \"ls\")\n=\u003e /bin /home /tmp /usr ...\n```\n\nFor convenience Cmd supplies the macro `with-working-directory`:\n\n``` lisp\n(with-working-directory (dir)\n  (cmd ...)\n  (cmd ...))\n≡ (progn\n    (cmd :in dir ...)\n    (cmd :in dir ...))\n```\n\n## The external program’s environment\n\nFor Unix users only, the variable `*cmd-env*` holds an alist of extra\nenvironment variables to set for each call to `cmd`.\n\n``` lisp\n\n(let ((*cmd-env* (acons \"GIT_PAGER\" \"cat\" *cmd-env*)))\n  (cmd \"git diff\" ...))\n```\n\nWe are currently very restrictive about what we consider a valid\nenvironment variable name.\n\n### Controlling PATH\n\nFor controlling the `PATH` environment variable, the Lisp variable\n`*cmd-path*` can be used:\n\n``` lisp\n(let ((*cmd-path* (cons #p\"~/.local/bin\" *cmd-path*)))\n   ...)\n```\n\nDirectories in `*cmd-path*` are prepended to `PATH`.\n\nThis uses the same mechanism as `*cmd-env*`, so it also only works on\nUnix.\n\n## Entry points\n\nThe `cmd` package offers several entry points:\n\n- `cmd` runs an external program synchronously, returning the exit\n  code. By default, on a non-zero exit it signals an error.\n\n  ```lisp\n  (cmd \"cat /etc/os-release\")\n  NAME=\"Ubuntu\" [...]\n  =\u003e 0\n  ```\n\n- `$cmd` returns the output of the external program as a string,\n  stripping any trailing newline. (Much like `$(cmd)` in a shell.) The\n  exit code is returned as a second value.\n\n  ```lisp\n  ($cmd \"date\")\n  =\u003e \"Sun Sep 27 15:43:01 CDT 2020\", 0\n  ```\n\n- `cmd!` runs an external program purely for side effects, discarding\n  all output and returning nothing. If the program exits non-zero,\n  however, it will still signal an error.\n\n- `cmd?` returns `t` if the external program returned `0`, and `nil`\n  otherwise, with the exit code as a second value. As other variants\n  by default signal an error if the process exists non-zero, `cmd?` is\n  useful for programs expected to fail.\n\n  ```lisp\n  (cmd? \"kill -0\" pid)\n  =\u003e T, 0   ;; PID is a live process\n  =\u003e NIL, 1 ;; PID is not a live process\n  ```\n\n- `cmd\u0026` runs an external program asynchronously (with\n  `uiop:launch-program`) and returns a UIOP `process-info` object.\n\n  ```lisp\n  (cmd\u0026 \"cp -a\" src dest)\n  =\u003e #\u003cPROCESS-INFO ...\u003e\n  ```\n\n## Error handling\n\nBy default, Cmd stores the stderr of a process, and if there is an\nerror (due to non-zero exit) it presents the stderr as part of the\nerror message.\n\nAccordingly `cmd` errors are a subclass of `uiop:subprocess-error`. The\nstored stderr can be accessed with `cmd:cmd-error-stderr`.\n\n## Redirection\n\nRedirection is accomplished via either tokenized strings or keyword\narguments. These should be self-explanatory to anyone who has used a\nshell.\n\n``` lisp\n;;; Using keyword arguments.\n(cmd \"echo 'hello world'\" :\u003e \"hello.txt\")\n(cmd \"cat hello.txt\")\n=\u003e hello world\n;; Append\n(cmd \"echo 'goodbye world'\" :\u003e\u003e \"hello.txt\")\n(cmd \"cat hello.txt\")\n=\u003e hello world\n   goodbye world\n(cmd \"tar cf - hello.txt\" :\u003e #p\"hello.tar\")\n(cmd \"rm hello.txt\")\n(cmd \"tar xf hello.tar\")\n(cmd \"cat hello.txt\")\n=\u003e hello world\ngoodbye world\n\n;;; Equivalents using tokenized strings.\n(cmd \"echo 'hello world' \u003e hello.txt\")\n(cmd \"cat hello.txt\")\n=\u003e hello world\n;; Append\n(cmd \"echo 'goodbye world' \u003e\u003e hello.txt\")\n(cmd \"cat hello.txt\")\n=\u003e hello world\ngoodbye world\n(cmd \"tar cf - hello.txt \u003e hello.tar\")\n(cmd \"rm hello.txt\")\n(cmd \"tar xf hello.tar\")\n(cmd \"cat hello.txt\")\n=\u003e hello world\ngoodbye world\n\n```\n\nRedirection with keyword arguments is usually more readable when the arguments are computed.\n\nSupported directions include:\n\n- `:\u003c` Redirect stdin.\n- `:\u003e`, `:1\u003e` Redirect stdout.\n- `:\u003e\u003e`, `:1\u003e\u003e` Append stdout.\n- `:2\u003e` Redirect stderr.\n- `:2\u003e\u003e` Append stderr.\n- `:\u0026\u003e`, `:\u003e\u0026` Redirect stdout and stderr.\n- `:\u0026\u003e\u003e`, `:\u003e\u003e\u0026` Append stdout and stderr.\n- `:\u003c\u003c\u003c` Provide input from a “here string”.\n\nNote redirections are interpreted according to the rules for Lisp\nkeywords (only the first occurrence of a keyword argument matters),\nnot the side-effecting rules for redirections in POSIX shells.\n\n### Pipelines\n\nThe simplest way to set up pipelines is to use tokenized strings:\n\n``` lisp\n(cmd \"cat /usr/share/dict/words | sort | uniq -c | sort -nrs | head -3\")\n=\u003e    1 a\n      1 A\n      1 Aachen\n```\n\nAlternately you can use keywords. While `:|\\||` is acceptable, you can write `\"|\"` instead. (Remember `\"|\"` will be tokenized to `'(:|\\||)`.)\n\n``` lisp\n(cmd \"cat /usr/share/dict/words\"\n     \"|\" '(\"sort\")\n     \"|\" '(\"uniq\" \"-c\")\n     \"|\" '(\"sort\" \"-nrs\")\n     \"|\" '(\"head\" \"-3\"))\n=\u003e    1 a\n      1 A\n      1 Aachen\n```\n\nAgain, separating out the pipeline symbols is usually more readable when the subcommands are computed.\n\n## Controlling cmd with hooks\n\nThere are two hooks you can use to control `cmd`. These are exported from the `cmd/hooks` package (so you can `:use :cmd` without having to worry about them.) Both hooks expect a list of functions of one argument.\n\nThe hook `*message-hook*` is called with the external program and its arguments, quoted as a shell command line. This can be useful for logging commands as they are run.\n\nThe hook `*proc-hook*` is called with the process object (as returned by `uiop:launch-program`). This can be useful if you want to be able to track what is being run in a particular dynamic extent.\n\n## Windows\n\nOn Windows only, the first argument (the program name) has `.exe` appended to it automatically if it doesn’t already have a file extension.\n\n## Efficiency\n\nWhile `cmd` does not use a shell to interpret its arguments, it may still have to run a shell (`sh` on Unix, `cmd.exe` on Windows) in order to change the working directory of the program.\n\nHow inefficient this is depends on what your distribution uses as `/bin/sh`; it is faster when `/bin/sh` is, say, `dash`, than when it is `bash`.\n\nRecent versions of GNU `env` support a `-C` switch to do this directly. When support is detected dynamically, then `env -C` is used in place of a shell and overhead is negligible.\n\n## Past\n\nCmd is a spinoff of [Overlord][], a Common Lisp build system, and was\ninspired by the `cmd` function in [Shake][], a Haskell build system,\nas well as the [Julia][] language’s [shell command\nfacility][backtick]. The `psub` function is inspired by the\n[builtin][psub] of the same name in the [Fish shell][].\n\n## Future\n\n- Pipelines should have “pipefail” behavior.\n- Pipelines should support stderr as well (`2|`, `\u0026|`).\n- Efferent process substitution should also be supported.\n- There should be a special variable holding an alist of extra\n  environment variables to set when running a command. (The problem\n  here is Windows.)\n\n[UIOP]: https://common-lisp.net/project/asdf/uiop.html\n[Overlord]: https://github.com/ruricolist/overlord\n[Shake]: https://shakebuild.com/\n[cl-shlex]: https://github.com/ruricolist/cl-shlex\n[dpd]: http://clhs.lisp.se/Body/v_defaul.htm\n[Bernstein chaining]: http://www.catb.organization/~eser/writings/taoup/html/ch06s06.html\n[Julia]: https://julialang.org\n[backtick]: https://julialang.org/blog/2013/04/put-this-in-your-pipe/\n[Fish shell]: https://fishshell.com\n[psub]: https://fishshell.com/docs/current/cmds/psub.html\n","funding_links":[],"categories":["Common Lisp","Interfaces to other package managers"],"sub_categories":["Third-party APIs"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruricolist%2Fcmd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fruricolist%2Fcmd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruricolist%2Fcmd/lists"}