diff --git a/.gitignore b/.gitignore index 385e7f8..c5afb05 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,6 @@ coverage.dat # --- Editor and OS noise --- .DS_Store -.vscode/ *~ \#*\# .\#* diff --git a/README.md b/README.md index e8ead87..22ae049 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ outside the tested support boundary. ## Development ```sh -nix develop # SBCL + cl-weave + paredit; registry already set +nix develop # SBCL with CL_SOURCE_REGISTRY already set nix run .#test # run the test suite nix flake check # full hermetic gate on x86_64-linux CI nix fmt # format Nix sources (treefmt) diff --git a/data/highlight-data.lisp b/data/presentation/highlight-data.lisp similarity index 76% rename from data/highlight-data.lisp rename to data/presentation/highlight-data.lisp index b760b58..6649e3d 100644 --- a/data/highlight-data.lisp +++ b/data/presentation/highlight-data.lisp @@ -1,7 +1,8 @@ -;;; Literal syntax-highlighting data kept separate from the classifier. -(in-package #:nshell.highlight) +;;; Highlight data tables: builtin names, operator token types, and the +;;; fallback (theme-less) ANSI role palette used by highlight.lisp. +(in-package #:nshell.presentation) -(defparameter +highlight-builtin-names+ +(defvar *builtin-commands* '("echo" "printf" "pwd" "ls" "cd" "exit" "fg" "bg" "jobs" "disown" "set" "export" "unset" "alias" "abbr" "function" "source" "exec" "true" "false" "contains" "test" "type" "which" "history" "help") diff --git a/docs/notes/coverage-analysis.md b/docs/notes/coverage-analysis.md index 9f9a580..2497787 100644 --- a/docs/notes/coverage-analysis.md +++ b/docs/notes/coverage-analysis.md @@ -16,20 +16,14 @@ silently removed from the report. ## Reproduce ```sh -NSHELL_COVERAGE_DIR="$PWD/coverage" \ +NSHELL_COVERAGE_DIR=/tmp/nshell-coverage \ nix develop 'path:.' --command sbcl --script scripts/coverage.lisp ``` -The command writes `cover-index.html` and `coverage-summary.json` below the -selected directory. In a Nix check, where `$out` is provided, the same files -are retained below `$out/coverage/`. Without an explicit directory, the script -uses `$TMPDIR/nshell-coverage/` when `TMPDIR` is available; the dev-shell -`coverage` alias selects the repository's `coverage/` directory explicitly. - The command is release evidence only when the selected test count is non-zero, -the test and error counts are zero, the generated HTML and JSON artifacts -exist, and both the minimum and target fields have been inspected. A passing -minimum with `target-reached=false` is a warning, not a 100% coverage claim. +the test and error counts are zero, the generated report exists, and both the +minimum and target fields have been inspected. A passing minimum with +`target-reached=false` is a warning, not a 100% coverage claim. ## What the suite verifies diff --git a/docs/notes/macro-consolidation-audit.md b/docs/notes/macro-consolidation-audit.md index 210ae7d..4c2ef93 100644 --- a/docs/notes/macro-consolidation-audit.md +++ b/docs/notes/macro-consolidation-audit.md @@ -10,7 +10,7 @@ Beyond `define-value-struct` (`docs/notes/value-struct-audit.md`), 26 other boilerplate categories this goal targets: - **Builtin command dispatch.** `define-builtin` - (`application/builtin-macros.lisp`) auto-generates ignore-declarations; + (`application/builtin-commands.lisp`) auto-generates ignore-declarations; `%with-option-arguments` / `%with-required-argument` (`application/builtin-runtime.lisp`) capture the shared "parse leading option flags, validate required args" skeleton; `%table-builtin-case` @@ -20,8 +20,8 @@ boilerplate categories this goal targets: (`application/builtin-string-support.lisp`) cover the `string` subcommand family; `define-test-predicate-table` (`application/builtin-test.lisp`) covers `test`/`[`. Registration itself is a single data table - (`+builtin-registry-specs+` in `data/application/builtin-spec-data.lisp`), - not hand-written per-command code. + (`+builtin-registry-specs+` in `application/builtin-spec-data.lisp`), not + hand-written per-command code. - **Value structs.** 37 value structs generate their accessor/predicate/constructor boilerplate from `define-value-struct` instead of by hand (`docs/notes/value-struct-audit.md`). diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index ba03207..022342f 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -80,7 +80,7 @@ git clone https://github.com/nerima-lisp/nshell cd nshell nix build # produces ./result/bin/nshell nix flake check # full hermetic gate on x86_64-linux CI -nix develop # dev shell with SBCL + cl-weave + paredit +nix develop # dev shell with SBCL + cl-weave ``` `flake.nix` declares `x86_64-linux` and `aarch64-darwin`. The full hermetic diff --git a/docs/src/guide/recipes.md b/docs/src/guide/recipes.md index 67bd5d3..b7d973d 100644 --- a/docs/src/guide/recipes.md +++ b/docs/src/guide/recipes.md @@ -53,21 +53,6 @@ Just the focused completion suite (`nshell/weave`): sbcl --script scripts/weave.lisp ``` -All repository entry points use `scripts/asdf-runtime.lisp` for source -discovery and compile policy. The Nix development shell supplies a complete -`CL_SOURCE_REGISTRY`; outside that shell, set `NSHELL_SOURCE_TREE` to an -explicit directory containing sibling Common Lisp systems, or set -`CL_SOURCE_REGISTRY` yourself. When no registry is supplied, the repository -itself is registered and the runtime does not scan a worktree's parent -directory. The shared runtime deliberately ignores inherited ASDF output -translations and writes compiled files below `NSHELL_ASDF_OUTPUT_DIR`, or a -temporary `nshell-asdf/` directory when that variable is unset: - -```sh -NSHELL_SOURCE_TREE=/path/to/common-lisp-systems \ - sbcl --script run-tests.lisp -``` - ### The non-sandboxed integration run Some cases need a real PTY, `stty`, and external binaries, which the Nix @@ -85,13 +70,11 @@ job-control lifecycle checks. ## Generate a coverage report ```sh -NSHELL_COVERAGE_DIR="$PWD/coverage" \ - nix develop -c sbcl --script scripts/coverage.lisp +nix develop -c sbcl --script scripts/coverage.lisp ``` -The report is written to `coverage/cover-index.html`, with the gate result in -`coverage/coverage-summary.json`. Set `NSHELL_COVERAGE_DIR` to redirect both -artifacts. The default Nix check stores them under its `$out/coverage/` output. +The report is written to `coverage/cover-index.html`. Set `NSHELL_COVERAGE_DIR` +to redirect the output. ## Performance evidence diff --git a/flake.nix b/flake.nix index f25d5a6..c53e4d9 100644 --- a/flake.nix +++ b/flake.nix @@ -552,14 +552,10 @@ # whole docs tree. treefmt.evalModule = treefmt-nix.lib.evalModule; - # The cl-weave CLI, which the suites' reporters are documented against, - # and the Paredit CLI used for structural Common Lisp analysis. Both - # come from the same pinned cl-weave input so the development shell does - # not grow a second, independently versioned tooling stack. - devShellPackages = ctx: [ - cl-weave.packages.${ctx.system}.default - cl-weave.inputs.paredit-cli.packages.${ctx.system}.default - ]; + # The cl-weave CLI, which the suites' reporters are documented against. + # Interactive only: the registry the shell exports already carries every + # system, check dependencies included. + devShellPackages = ctx: [ cl-weave.packages.${ctx.system}.default ]; overrideOutputs = ctx: @@ -580,17 +576,14 @@ devShells.default = ctx.generated.devShells.default.overrideAttrs (previous: { shellHook = previous.shellHook + '' export NSHELL_ROOT=$PWD - export NSHELL_TIMEOUT_SECONDS=1800 - alias test='cd "$NSHELL_ROOT" && timeout --signal=TERM --kill-after=5s "$NSHELL_TIMEOUT_SECONDS" sbcl --script "$NSHELL_ROOT/run-tests.lisp"' - alias coverage='cd "$NSHELL_ROOT" && NSHELL_COVERAGE_DIR="$NSHELL_ROOT/coverage" timeout --signal=TERM --kill-after=5s "$NSHELL_TIMEOUT_SECONDS" sbcl --script "$NSHELL_ROOT/scripts/coverage.lisp"' - alias weave='cd "$NSHELL_ROOT" && timeout --signal=TERM --kill-after=5s "$NSHELL_TIMEOUT_SECONDS" sbcl --script "$NSHELL_ROOT/scripts/weave.lisp"' - alias paredit='paredit' + alias test='cd "$NSHELL_ROOT" && sbcl --script "$NSHELL_ROOT/run-tests.lisp"' + alias coverage='cd "$NSHELL_ROOT" && NSHELL_COVERAGE_DIR="$NSHELL_ROOT/coverage" sbcl --script "$NSHELL_ROOT/scripts/coverage.lisp"' + alias weave='cd "$NSHELL_ROOT" && sbcl --script "$NSHELL_ROOT/scripts/weave.lisp"' echo "" echo "nshell development environment" echo " test - Run the full nshell suite (cl-weave, nshell/test)" echo " weave - Run the focused completion suite (nshell/weave)" echo " coverage - Run the test suite and write HTML coverage to coverage/" - echo " paredit - Run Paredit-aware Common Lisp analysis" echo " sbcl - Interactive Common Lisp (with cl-weave)" echo "" ''; @@ -632,8 +625,8 @@ # Generate the sb-cover report and enforce the configured source # expression minimum. The report also records the distance from - # the explicit 100% target without claiming that it has been - # reached when executable expressions remain uncovered. + # the aspirational 100% target without making an unsupported + # claim that structural sb-cover forms are executable. coverage = ctx.cl.mkScriptCheck { drv = ctx.package; entryPoint = "scripts/coverage.lisp"; diff --git a/nshell.asd b/nshell.asd index e02a241..2775636 100644 --- a/nshell.asd +++ b/nshell.asd @@ -1,3 +1,11 @@ +;;; This form comes FIRST, before any defsystem. ASDF binds *package* to +;;; ASDF-USER only for a file it loads itself; read any other way — a REPL +;;; `load`, an editor evaluating the buffer, flake.nix parsing :version — the +;;; file is read in whatever package happens to be current, and an unqualified +;;; `defsystem` then fails to read at all. Saying it makes the file +;;; self-contained. See PACKAGE_STANDARD.md "asd の書き方". +(in-package #:asdf-user) + ;;; Metadata keys follow the org's canonical order: ;;; :description :long-description :author :maintainer :license :version ;;; :homepage :bug-tracker :source-control :depends-on :pathname :serial @@ -37,7 +45,6 @@ (:file "package-domain") (:file "package-application") (:file "package-infrastructure") - (:file "package-highlight") (:file "package-presentation") (:module "feature-command-line" :pathname "../packages/feature/command-line/src" @@ -55,11 +62,11 @@ (:file "domain/execution/command") (:file "domain/execution/pipeline") (:file "domain/execution/job") - (:file "domain/parsing/ast") - (:file "domain/parsing/ast-redirect-split") - (:file "domain/parsing/tokenizer-data") - (:file "domain/parsing/tokenizer-readers") - (:file "domain/parsing/tokenizer-handlers") + (:file "domain/parsing/ast") + (:file "domain/parsing/ast-redirect-split") + (:file "domain/parsing/tokenizer-data") + (:file "domain/parsing/tokenizer-readers") + (:file "domain/parsing/tokenizer-handlers") (:file "domain/parsing/parse-result") (:file "domain/parsing/control-flow-data") (:file "domain/parsing/control-flow") @@ -74,18 +81,18 @@ (:file "domain/parsing/parser-here-doc") (:file "domain/parsing/parser-reduction") (:file "domain/parsing/parser") - (:file "domain/environment/env") - (:file "domain/expansion/expand") - (:file "domain/expansion/brace") - (:file "domain/expansion/parameter-data") - (:file "domain/expansion/parameter-selection") - (:file "domain/expansion/parameter-braced") - (:file "domain/expansion/parameter") - (:file "domain/expansion/arithmetic") - (:file "domain/expansion/arithmetic-operator-data" - :pathname "../data/domain/expansion/arithmetic-operator-data") - (:file "domain/expansion/fields") - (:file "domain/abbreviation/expansion") + (:file "domain/environment/env") + (:file "domain/expansion/expand") + (:file "domain/expansion/brace") + (:file "domain/expansion/parameter-data") + (:file "domain/expansion/parameter-selection") + (:file "domain/expansion/parameter-braced") + (:file "domain/expansion/parameter") + (:file "domain/expansion/arithmetic") + (:file "domain/expansion/arithmetic-operator-data" + :pathname "../data/domain/expansion/arithmetic-operator-data") + (:file "domain/expansion/fields") + (:file "domain/abbreviation/expansion") (:file "domain/completion/candidate") (:file "domain/completion/catalog-build") (:file "domain/completion/catalog-command-data" @@ -111,27 +118,23 @@ (:file "domain/prompting/prompt") (:file "application/event-dispatcher") (:file "application/shell-context") - (:file "application/execute-pipeline-expansion") - (:file "application/execute-pipeline-redirect") - (:file "application/execute-pipeline-stage-external") - (:file "application/execute-pipeline-stage-background") - (:file "application/execute-pipeline-stage") - (:file "application/execute-pipeline-control") + (:file "application/execute-pipeline-expansion") + (:file "application/execute-pipeline-redirect") + (:file "application/execute-pipeline-stage-external") + (:file "application/execute-pipeline-stage-background") + (:file "application/execute-pipeline-stage") + (:file "application/execute-pipeline-control") (:file "application/manage-job") (:file "application/pipeline-diagram") (:file "application/builtin-spec-data" :pathname "../data/application/builtin-spec-data") (:file "application/builtin-spec") (:file "application/builtin-runtime") - (:file "application/builtin-macros") (:file "application/builtin-type-helpers") (:file "application/builtin-string-support") (:file "application/builtin-string") (:file "application/builtin-source-reader") (:file "application/builtin-source") - (:file "application/builtin-printf-format") - (:file "application/builtin-printf-parser") - (:file "application/builtin-printf") (:file "application/builtin-commands") (:file "application/builtin-commands-history") (:file "application/builtin-jobs") @@ -156,7 +159,6 @@ (:file "infrastructure/acl/pty") (:file "infrastructure/acl/pty-spawn") (:file "infrastructure/acl/signal-acl") - (:file "infrastructure/acl/process-capabilities") (:file "infrastructure/persistence/file-history") (:file "infrastructure/persistence/file-config") (:file "infrastructure/terminal/raw-mode") @@ -164,9 +166,6 @@ (:file "infrastructure/terminal/input-core") (:file "infrastructure/terminal/input-decode") (:file "infrastructure/terminal/input-read") - (:file "highlight-data" - :pathname "../data/highlight-data") - (:file "highlight") (:file "presentation/input-state-static-data" :pathname "../data/presentation/input-state-data") (:file "presentation/input-state-core") @@ -185,20 +184,23 @@ (:file "presentation/input-state-kill-yank") (:file "presentation/input-state-history-search") (:file "presentation/input-state-dispatch") - (:file "presentation/input-state-vi-data") - (:file "presentation/input-state-vi") - (:file "presentation/input-state-vi-edit") + (:file "presentation/input-state-vi-data") + (:file "presentation/input-state-vi") + (:file "presentation/input-state-vi-edit") (:file "presentation/input-state-session") (:file "presentation/repl-boundaries") (:file "presentation/prompt-display") (:file "presentation/completion-ui") (:file "presentation/autosuggest") - (:file "presentation/repl-state") - (:file "presentation/repl-input-state") - (:file "presentation/repl-completion-seed") - (:file "presentation/repl-environment") - (:file "presentation/repl-session-init") - (:file "presentation/repl-process") + (:file "presentation/highlight-data" + :pathname "../data/presentation/highlight-data") + (:file "presentation/highlight") + (:file "presentation/repl-state") + (:file "presentation/repl-input-state") + (:file "presentation/repl-completion-seed") + (:file "presentation/repl-environment") + (:file "presentation/repl-session-init") + (:file "presentation/repl-process") (:file "presentation/repl-execution-context") (:file "presentation/repl-execution-command") (:file "presentation/repl-execution") @@ -210,10 +212,10 @@ (:file "presentation/repl-output-handlers") (:file "presentation/repl-output-event-handlers") (:file "presentation/repl-output") - (:file "presentation/repl-session") - (:file "presentation/repl-batch") - (:file "presentation/repl") - (:file "main")) + (:file "presentation/repl-session") + (:file "presentation/repl-batch") + (:file "presentation/repl") + (:file "main")) ;; The three build keys and the :perform below are exempt from the metadata ;; order above -- PACKAGE_STANDARD.md names cl-weave's identical trio and ;; cl-tty-kit's :perform as "はどこに書いても構いません" -- and they sit here, @@ -274,16 +276,16 @@ ((:file "package") (:file "support/assertions") (:file "helpers-runner") - (:file "support/pbt") - (:file "support/pbt-shell") - (:file "support/input-state") - (:file "support/input-state-assertions") + (:file "support/pbt") + (:file "support/pbt-shell") + (:file "support/input-state") + (:file "support/input-state-assertions") (:file "support/repl") (:file "support/builtins") (:file "support/completion") - (:file "support/prompt") - (:file "support/history") - (:file "support/matchers") + (:file "support/prompt") + (:file "support/history") + (:file "support/matchers") (:file "unit/test-package-by-feature") (:file "unit/test-domain-events") (:file "unit/test-signals") @@ -300,13 +302,13 @@ (:file "unit/test-expansion-abbreviation") (:file "unit/test-completion-rules") (:file "unit/test-completion-rule-prover-boundaries") - (:file "unit/test-completion-rule-prover") - (:file "unit/test-completion-builtins") - (:file "unit/test-completion-knowledge-base") - (:file "unit/test-completion-path-reducer") - (:file "unit/test-completion-knowledge-base-behavior") - (:file "unit/test-completion-properties") - (:file "unit/test-completion-context") + (:file "unit/test-completion-rule-prover") + (:file "unit/test-completion-builtins") + (:file "unit/test-completion-knowledge-base") + (:file "unit/test-completion-path-reducer") + (:file "unit/test-completion-knowledge-base-behavior") + (:file "unit/test-completion-properties") + (:file "unit/test-completion-context") (:file "unit/test-repl") (:file "unit/test-repl-completion-data") (:file "unit/test-repl-background") diff --git a/run-tests.lisp b/run-tests.lisp index 9d392f6..51df9de 100644 --- a/run-tests.lisp +++ b/run-tests.lisp @@ -7,30 +7,38 @@ ;;;; the hermetic Nix check and a plain local run execute exactly the same code ;;;; path rather than two hand-rolled `--eval` chains that drift apart. ;;;; -;;;; Dependency resolution is shared with the auxiliary scripts through -;;;; scripts/asdf-runtime.lisp. An explicit CL_SOURCE_REGISTRY still wins; -;;;; otherwise NSHELL_SOURCE_TREE opts into one explicit sibling-system tree. +;;;; Dependency resolution mirrors scripts/weave.lisp: inside `nix develop` or +;;;; the Nix sandbox the systems are already on CL_SOURCE_REGISTRY, and for a +;;;; plain ghq checkout the parent directory tree is registered so sibling +;;;; checkouts (../cl-weave, ../cl-prolog-kit, ...) are found automatically. An +;;;; explicit CL_SOURCE_REGISTRY still wins, because the existing configuration +;;;; is inherited rather than replaced. (require :asdf) +(asdf:load-system :cl-host-kit) -(let* ((script-path (truename (or *load-truename* - *load-pathname* - #P"./run-tests.lisp"))) - (root (uiop:pathname-directory-pathname script-path))) - (load (merge-pathnames #P"scripts/asdf-runtime.lisp" root)) - (format *error-output* "~&[nshell] configure runtime~%") - (finish-output *error-output*) - (nshell-configure-runtime root) - (format *error-output* "~&[nshell] load nshell/test~%") - (finish-output *error-output*) - (asdf:load-system "nshell/test") - (format *error-output* "~&[nshell] run tests~%") - (finish-output *error-output*) - (let ((runner (find-symbol "RUN-TESTS" "NSHELL/TEST"))) - (unless (and runner (fboundp runner)) - (error "NSHELL/TEST:RUN-TESTS is not available.")) - (unless (funcall runner) - (error "nshell/test reported failure."))) - (format *error-output* "~&[nshell] tests passed~%") - (finish-output *error-output*) - (sb-ext:exit :code 0)) +(let* ((root (truename #P"./")) + (parent (host-kit:parent-directory-pathname root))) + (asdf:initialize-source-registry + (if (host-kit:getenv "CL_SOURCE_REGISTRY") + `(:source-registry + (:directory ,root) + :inherit-configuration) + `(:source-registry + (:directory ,root) + (:tree ,parent) + :inherit-configuration))) + ;; Warn rather than abort on compile-file warnings: the suite's own failures + ;; are the signal this script reports, and a style warning in a dependency + ;; should not masquerade as a test failure. + (setf asdf:*compile-file-warnings-behaviour* :warn + asdf:*compile-file-failure-behaviour* :warn) + (let ((passed-p + (handler-case + (progn + (asdf:load-system "nshell/test") + (funcall (find-symbol "RUN-TESTS" "NSHELL/TEST"))) + (error (condition) + (format *error-output* "~&nshell/test failed: ~A~%" condition) + nil)))) + (sb-ext:exit :code (if passed-p 0 1)))) diff --git a/scripts/asdf-runtime.lisp b/scripts/asdf-runtime.lisp index 12e462e..d70ff11 100644 --- a/scripts/asdf-runtime.lisp +++ b/scripts/asdf-runtime.lisp @@ -16,113 +16,3 @@ (t ,(truename output-root)) :ignore-inherited-configuration)) output-root)) - -(defun %nshell-register-asd-file (registry source) - (let ((name (string-downcase (pathname-name source)))) - (unless (gethash name registry) - (setf (gethash name registry) source)))) - -(defparameter +nshell-source-registry-exclusions+ - '(".bzr" ".cdv" ".git" ".hg" ".pc" ".svn" "CVS" "RCS" "SCCS" - "_darcs" "_sgbak" "autom4te.cache" "cover_db" "_build" "debian")) - -(defun %nshell-register-asd-directory (registry directory) - (dolist (source (asdf/source-registry:directory-asd-files directory)) - (%nshell-register-asd-file registry source))) - -(defun %nshell-register-asd-tree (registry directory exclusions) - (asdf/source-registry:collect-sub*directories-asd-files - directory - :exclude exclusions - :collect (lambda (source) - (%nshell-register-asd-file registry source)))) - -(defun %nshell-process-source-registry-form (registry form) - (let ((exclusions (copy-list +nshell-source-registry-exclusions+))) - (dolist (directive - (cdr (asdf/source-registry:validate-source-registry-form form))) - (destructuring-bind (keyword &rest arguments) - (if (consp directive) directive (list directive)) - (case keyword - (:directory - (%nshell-register-asd-directory registry - (uiop:ensure-directory-pathname - (pathname (first arguments))))) - (:tree - (%nshell-register-asd-tree registry - (uiop:ensure-directory-pathname - (pathname (first arguments))) - exclusions)) - (:exclude - (setf exclusions (copy-list arguments))) - (:also-exclude - (setf exclusions (append exclusions arguments))) - ((:ignore-inherited-configuration :inherit-configuration) - nil) - (:default-registry - (error "The nshell source registry cannot include the default registry.")) - (:include - (error "The nshell source registry cannot include ~S." (first arguments))) - (otherwise - (error "Unsupported nshell source-registry directive ~S." keyword))))))) - -(defun %nshell-build-source-registry (forms) - "Build an ASDF registry from FORMS without traversing implementation trees. - -ASDF's standard initializer wraps every registry with the implementation's -source tree. In the Nix shell that tree can contain recursive editor links, -so direct registration is the deterministic project boundary we need here. -Only explicit directories and trees are accepted; inherited and user-level -registries would make a build depend on the host machine." - (let ((registry (make-hash-table :test #'equal))) - (dolist (form forms) - (%nshell-process-source-registry-form registry form)) - registry)) - -(defun nshell-configure-source-registry (root) - "Register ROOT and an explicitly requested source tree with ASDF. - -An explicit CL_SOURCE_REGISTRY is appended after ROOT so project definitions -win name collisions. Outside that shell, NSHELL_SOURCE_TREE is opt-in -because a worktree parent can contain many unrelated checkouts and scanning it -makes source discovery both slow and nondeterministic." - (let* ((root (truename (uiop:ensure-directory-pathname root))) - (forms (list `(:source-registry - (:directory ,root) - :ignore-inherited-configuration))) - (source-registry (uiop:getenv "CL_SOURCE_REGISTRY"))) - (if source-registry - (setf forms - (nconc forms - (list - (asdf/source-registry:parse-source-registry-string - source-registry)))) - (let ((source-tree (uiop:getenv "NSHELL_SOURCE_TREE"))) - (when source-tree - (setf forms - (nconc forms - (list `(:source-registry - (:tree ,(truename - (uiop:ensure-directory-pathname - source-tree)))))))))) - (setf asdf/source-registry:*source-registry* - (%nshell-build-source-registry forms)) - root)) - -(defun nshell-configure-runtime (root) - "Configure ASDF source lookup and compile policy. - -Compiled systems are written beneath NSHELL_ASDF_OUTPUT_DIR when it is set, -or beneath the host temporary directory otherwise. This is required when the -source tree or its dependencies are read-only, as they are in a Nix build. - -SB-POSIX is required here, before the source registry below is replaced with -the project-only tree: cl-host-kit depends on it as a plain ASDF system name -(not a (:require ...) component), and its .asd file lives in SBCL's own -contrib tree, which the replacement registry deliberately excludes." - (require :sb-posix) - (let ((root (nshell-configure-source-registry root))) - (nshell-configure-writable-asdf-output) - (setf asdf:*compile-file-warnings-behaviour* :warn - asdf:*compile-file-failure-behaviour* :error) - root)) diff --git a/scripts/benchmark-completion.lisp b/scripts/benchmark-completion.lisp index 02c4091..f5f0ebe 100644 --- a/scripts/benchmark-completion.lisp +++ b/scripts/benchmark-completion.lisp @@ -1,43 +1,47 @@ (require :asdf) +(asdf:load-system :cl-host-kit) -(let* ((script-path (truename (or *load-truename* - *load-pathname* - #P"./scripts/benchmark-completion.lisp"))) - (script-directory (uiop:pathname-directory-pathname script-path)) - (root (uiop:pathname-parent-directory-pathname script-directory))) - (load (merge-pathnames #P"asdf-runtime.lisp" script-directory)) - (nshell-configure-runtime root) - (let* ((jsonl-path (uiop:getenv "NSHELL_BENCH_JSONL")) - (mode (string-downcase (or (uiop:getenv "NSHELL_BENCH_MODE") "warm")))) - (handler-case - (progn - (unless (member mode '("warm" "process" "all") :test #'string=) - (error "NSHELL_BENCH_MODE must be warm, process, or all (got ~S)" mode)) - (asdf:load-system :nshell/benchmark :force t) - (labels ((run (stream) - (when (member mode '("warm" "all") :test #'string=) - (funcall (find-symbol "RUN-COMPLETION-BENCHMARK" "NSHELL/BENCHMARK") - :jsonl-stream stream)) - (when (member mode '("process" "all") :test #'string=) - (funcall (find-symbol "RUN-PROCESS-BENCHMARK" "NSHELL/BENCHMARK") - :jsonl-stream (or stream *standard-output*))))) - (if (and jsonl-path (plusp (length jsonl-path))) - (progn - (format t "~&JSONL output: ~A~%" jsonl-path) - (let ((failure nil)) - (with-open-file (stream jsonl-path - :direction :output - :if-exists :supersede - :if-does-not-exist :create - :external-format :utf-8) - (handler-case - (run stream) - (error (condition) - (setf failure condition)))) - (when failure - (error failure)))) - (run nil))) - (sb-ext:exit :code 0)) - (error (condition) - (format *error-output* "~&nshell benchmark failed: ~A~%" condition) - (sb-ext:exit :code 1))))) +(let* ((root (truename #P"./")) + (parent (host-kit:parent-directory-pathname root)) + (jsonl-path (host-kit:getenv "NSHELL_BENCH_JSONL")) + (mode (string-downcase (or (host-kit:getenv "NSHELL_BENCH_MODE") "warm")))) + (asdf:initialize-source-registry + (if (host-kit:getenv "CL_SOURCE_REGISTRY") + (list :source-registry + (list :directory root) + :inherit-configuration) + (list :source-registry + (list :directory root) + (list :tree parent) + :inherit-configuration))) + (setf asdf:*compile-file-warnings-behaviour* :warn + asdf:*compile-file-failure-behaviour* :warn) + (handler-case + (progn + (unless (member mode '("warm" "process" "all") :test #'string=) + (error "NSHELL_BENCH_MODE must be warm, process, or all (got ~S)" mode)) + (asdf:load-system :nshell/benchmark :force t) + (labels ((run (stream) + (when (member mode '("warm" "all") :test #'string=) + (funcall (find-symbol "RUN-COMPLETION-BENCHMARK" "NSHELL/BENCHMARK") + :jsonl-stream stream)) + (when (member mode '("process" "all") :test #'string=) + (funcall (find-symbol "RUN-PROCESS-BENCHMARK" "NSHELL/BENCHMARK") + :jsonl-stream (or stream *standard-output*))))) + (if (and jsonl-path (plusp (length jsonl-path))) + (progn + (format t "~&JSONL output: ~A~%" jsonl-path) + (let ((failure nil)) + (with-open-file (stream jsonl-path :direction :output :if-exists :supersede + :if-does-not-exist :create :external-format :utf-8) + (handler-case + (run stream) + (error (condition) + (setf failure condition)))) + (when failure + (error failure)))) + (run nil))) + (sb-ext:exit :code 0)) + (error (condition) + (format *error-output* "~&nshell benchmark failed: ~A~%" condition) + (sb-ext:exit :code 1)))) diff --git a/scripts/coverage.lisp b/scripts/coverage.lisp index 74ee94d..417d2dc 100644 --- a/scripts/coverage.lisp +++ b/scripts/coverage.lisp @@ -4,9 +4,11 @@ ;;;; ;;;; nshell/test depends on sibling nerima-lisp toolkit checkouts (cl-prolog-kit, ;;;; cl-parser-kit, ...). Inside `nix develop` those systems are already on the -;;;; ASDF source registry. Outside that shell, set NSHELL_SOURCE_TREE to one -;;;; explicit directory containing those systems, or set CL_SOURCE_REGISTRY -;;;; yourself. The shared runtime never scans a worktree parent directory. +;;;; ASDF source registry. For a plain local checkout we also register the +;;;; parent directory tree, so sibling ghq checkouts are discovered +;;;; automatically. When CL_SOURCE_REGISTRY is present (as it is in the Nix +;;;; development shell), register only this checkout and inherit that explicit +;;;; list. Without it, register the parent tree for a plain local checkout. ;;;; ;;;; :force :all is required, not :force t: :force t only forces recompiling ;;;; nshell/test itself, leaving the nshell (src/) dependency loaded from @@ -14,13 +16,13 @@ ;;;; reaches src/, and the report silently covers only test files. (require :asdf) -(defparameter *nshell-coverage-script-path* - (truename (or *load-truename* *load-pathname* #P"./scripts/coverage.lisp"))) - (load (merge-pathnames #P"asdf-runtime.lisp" - (uiop:pathname-directory-pathname *nshell-coverage-script-path*))) + (uiop:pathname-directory-pathname + (or *load-truename* *load-pathname*)))) +(nshell-configure-writable-asdf-output) + (require :sb-cover) (declaim (optimize sb-cover:store-coverage-data)) @@ -28,11 +30,6 @@ (progn (defun %coverage-string-prefix-p (prefix string) (and (<= (length prefix) (length string)) (string= prefix string :end2 (length prefix)))) - (defun %coverage-call (function &rest arguments) - (handler-case - (values (apply function arguments) nil) - (error (condition) - (values nil condition)))) (defun %next-coverage-row (text start end) (let ((odd (search "" text :start2 start :end2 end)) (even (search "" text :start2 start :end2 end))) @@ -56,22 +53,15 @@ (search total-marker row :start2 (+ covered-position (length covered-marker)))))) (if (or (null covered-position) (null total-position)) (values nil nil next-row) (values - (nth-value 0 - (%coverage-call - #'parse-integer - row - :start - (+ covered-position (length covered-marker)) - :junk-allowed - t)) - (nth-value 0 - (%coverage-call - #'parse-integer - row - :start - (+ total-position (length total-marker)) - :junk-allowed - t)) + (ignore-errors + (parse-integer + row + :start + (+ covered-position (length covered-marker)) + :junk-allowed + t)) + (ignore-errors + (parse-integer row :start (+ total-position (length total-marker)) :junk-allowed t)) next-row)))))) (defun %coverage-row-file-name (text start end) (let* ((row-end (search "" text :start2 start :end2 end)) @@ -140,26 +130,12 @@ (let ((raw (uiop:getenv name))) (if (null raw) default (let ((*read-eval* nil) - (value (nth-value 0 (%coverage-call #'read-from-string raw)))) + (value (ignore-errors (read-from-string raw)))) (if (and (realp value) (<= 0 value) (<= value 100)) (float value 1.0) (error "Invalid ~A value ~S; expected a number from 0 to 100." name raw)))))) (defun %json-boolean (value) (if value "true" "false")) - (defun %default-coverage-directory (root) - (let ((nix-output (uiop:getenv "out")) - (tmpdir (uiop:getenv "TMPDIR"))) - (cond - (nix-output - (merge-pathnames - #P"coverage/" - (uiop:ensure-directory-pathname nix-output))) - (tmpdir - (merge-pathnames - #P"nshell-coverage/" - (uiop:ensure-directory-pathname tmpdir))) - (t - (merge-pathnames #P"coverage/" root))))) (defun %write-coverage-summary (pathname files covered @@ -189,14 +165,17 @@ (%json-boolean tests-passed) (%json-boolean (and (plusp total) (>= percentage minimum))) (%json-boolean (and (plusp total) (>= percentage target)))))) - (let* ((root (uiop:pathname-parent-directory-pathname - (uiop:pathname-directory-pathname - *nshell-coverage-script-path*))) + (let* ((root (truename #P"./")) + (parent (uiop:pathname-parent-directory-pathname root)) + (tmpdir (uiop:getenv "TMPDIR")) (coverage-dir (uiop:ensure-directory-pathname (or (uiop:getenv "NSHELL_COVERAGE_DIR") - (%default-coverage-directory root)))) + (if tmpdir (merge-pathnames + #P"nshell-coverage/" + (uiop:ensure-directory-pathname tmpdir)) + (merge-pathnames #P"coverage/" root))))) (index-path (merge-pathnames #P"cover-index.html" coverage-dir)) (summary-path (merge-pathnames #P"coverage-summary.json" coverage-dir)) (source-root (uiop:native-namestring (truename (merge-pathnames #P"src/" root)))) @@ -205,7 +184,15 @@ (tests-passed nil) (report-passed nil) (coverage-passed nil)) - (nshell-configure-runtime root) + (asdf:initialize-source-registry + (if (uiop:getenv "CL_SOURCE_REGISTRY") + `(:source-registry + (:directory ,root) + :inherit-configuration) + `(:source-registry + (:directory ,root) + (:tree ,parent) + :inherit-configuration))) (ensure-directories-exist coverage-dir) (sb-cover:enable-coverage-logging) (unwind-protect (setf tests-passed (handler-case @@ -265,7 +252,7 @@ (unless coverage-passed (format *error-output* - "~&src executable expression coverage did not meet the required minimum (~,2F%%).~%" + "~&src executable expression coverage did not meet the configured minimum (~,2F%%).~%" minimum)))) (sb-ext:exit :code diff --git a/scripts/weave.lisp b/scripts/weave.lisp index e9a9984..05da621 100644 --- a/scripts/weave.lisp +++ b/scripts/weave.lisp @@ -3,18 +3,35 @@ ;;;; Usage: sbcl --script scripts/weave.lisp ;;;; ;;;; Beyond cl-weave and cl-prolog-kit, this suite also depends on cl-prolog-kit/weave. -;;;; The ASDF source and output configuration is shared with run-tests.lisp and -;;;; the coverage and benchmark entry points. +;;;; Inside `nix develop` those systems are already on the +;;;; ASDF source registry. For a plain local checkout we also register the +;;;; parent directory tree, so sibling ghq checkouts (../cl-weave, ../cl-prolog-kit) +;;;; are discovered automatically. An explicit CL_SOURCE_REGISTRY still wins +;;;; because we inherit the existing configuration. (require :asdf) +(asdf:load-system :cl-host-kit) -(let* ((script-path (truename (or *load-truename* - *load-pathname* - #P"./scripts/weave.lisp"))) - (script-directory (uiop:pathname-directory-pathname script-path)) - (root (uiop:pathname-parent-directory-pathname script-directory))) - (load (merge-pathnames #P"asdf-runtime.lisp" script-directory)) - (nshell-configure-runtime root) +(load + (merge-pathnames + #P"asdf-runtime.lisp" + (uiop:pathname-directory-pathname + (or *load-truename* *load-pathname*)))) +(nshell-configure-writable-asdf-output) + +(let* ((root (truename #P"./")) + (parent (host-kit:parent-directory-pathname root))) + (asdf:initialize-source-registry + (if (host-kit:getenv "CL_SOURCE_REGISTRY") + `(:source-registry + (:directory ,root) + :inherit-configuration) + `(:source-registry + (:directory ,root) + (:tree ,parent) + :inherit-configuration))) + (setf asdf:*compile-file-warnings-behaviour* :warn + asdf:*compile-file-failure-behaviour* :warn) (let ((passed-p (handler-case (progn diff --git a/src/application/builtin-commands.lisp b/src/application/builtin-commands.lisp index 94ab051..3296967 100644 --- a/src/application/builtin-commands.lisp +++ b/src/application/builtin-commands.lisp @@ -1,8 +1,320 @@ (in-package #:nshell.application) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (defmacro define-builtin (name lambda-list ignore-variables &body body) + `(defun ,name ,lambda-list + ,@(when ignore-variables + `((declare (ignore ,@ignore-variables)))) + ,@body))) + (define-builtin %builtin-echo (context args) (context) (values (format nil "~{~a~^ ~}~%" args) 0)) +(defun %printf-pad (text width left-p &optional (pad-character #\Space)) + (let ((padding (max 0 (- (or width 0) (length text))))) + (if (zerop padding) + text + (let ((padding-text (make-string padding :initial-element pad-character))) + (if left-p + (concatenate 'string text padding-text) + (concatenate 'string padding-text text)))))) + +(defun %printf-flag-p (flags flag) + (find flag flags :test #'char=)) + +(defun %printf-read-escape (text index) + (if (>= index (length text)) + (values "\\" index nil) + (let ((character (char text index))) + (case character + (#\a (values (string (code-char 7)) (1+ index) nil)) + (#\b (values (string #\Backspace) (1+ index) nil)) + (#\c (values "" (1+ index) t)) + (#\e (values (string (code-char 27)) (1+ index) nil)) + (#\f (values (string #\Page) (1+ index) nil)) + (#\n (values (string #\Newline) (1+ index) nil)) + (#\r (values (string #\Return) (1+ index) nil)) + (#\t (values (string #\Tab) (1+ index) nil)) + (#\v (values (string (code-char 11)) (1+ index) nil)) + (#\\ (values (string #\\) (1+ index) nil)) + (#\0 + (let ((cursor (1+ index)) + (value 0) + (digits 0)) + (loop while (and (< cursor (length text)) + (< digits 3) + (find (char text cursor) "01234567" :test #'char=)) + do (setf value (+ (* value 8) + (- (char-code (char text cursor)) + (char-code #\0))) + cursor (1+ cursor) + digits (1+ digits))) + (values (string (or (code-char value) #\Null)) cursor nil))) + (otherwise (values (string character) (1+ index) nil)))))) + +(defun %printf-expand-escapes (text) + (let* ((stop nil) + (result + (with-output-to-string (out) + (loop with index = 0 + while (< index (length text)) + do (if (char= (char text index) #\\) + (multiple-value-bind (replacement next-index stop-p) + (%printf-read-escape text (1+ index)) + (write-string replacement out) + (setf index next-index + stop stop-p) + (when stop + (return))) + (progn + (write-char (char text index) out) + (incf index))))))) + (values result stop))) + +(defun %printf-parse-integer (argument) + (handler-case + (values (parse-integer argument :junk-allowed nil) t) + (error () (values 0 nil)))) + +(defun %printf-parse-real (argument) + (handler-case + (let ((*read-eval* nil)) + (multiple-value-bind (value position) + (read-from-string argument nil nil) + (if (and (realp value) + (= position (length argument))) + (values (coerce value 'double-float) t) + (values 0d0 nil)))) + (error () (values 0d0 nil)))) + +(defun %printf-integer-text (number conversion flags precision) + (let* ((negative (minusp number)) + (unsigned (if negative (abs number) number)) + (base (case conversion + ((#\x #\X) 16) + (#\o 8) + (otherwise 10))) + (digits (case base + (16 (format nil "~x" unsigned)) + (8 (format nil "~o" unsigned)) + (otherwise (format nil "~d" unsigned)))) + (digits (if (char= conversion #\X) + (string-upcase digits) + digits)) + (digits (if precision + (concatenate 'string + (make-string (max 0 (- precision (length digits))) + :initial-element #\0) + digits) + digits)) + (prefix (if (and (%printf-flag-p flags #\#) + (not (zerop unsigned))) + (case conversion + (#\x "0x") + (#\X "0X") + (#\o "0") + (otherwise "")) + "")) + (sign (cond + (negative "-") + ((%printf-flag-p flags #\+) "+") + ((%printf-flag-p flags #\Space) " ") + (t "")))) + (concatenate 'string sign prefix digits))) + +(defun %printf-format-value (argument conversion flags width precision) + (case conversion + (#\s + (values (%printf-pad (if precision + (subseq argument 0 (min precision (length argument))) + argument) + width + (%printf-flag-p flags #\-)) + t + nil)) + (#\b + (multiple-value-bind (text stop-p) (%printf-expand-escapes argument) + (values (%printf-pad (if precision + (subseq text 0 (min precision (length text))) + text) + width + (%printf-flag-p flags #\-)) + t + stop-p))) + (#\c + (values (%printf-pad (if (plusp (length argument)) + (string (char argument 0)) + (string #\Null)) + width + (%printf-flag-p flags #\-)) + t + nil)) + ((#\d #\i #\u #\o #\x #\X) + (multiple-value-bind (number valid-p) (%printf-parse-integer argument) + (values (%printf-pad (%printf-integer-text number conversion flags precision) + width + (%printf-flag-p flags #\-) + (if (and (%printf-flag-p flags #\0) + (not (%printf-flag-p flags #\-)) + (null precision)) + #\0 + #\Space)) + valid-p + nil))) + ((#\e #\E #\f #\g #\G) + (multiple-value-bind (number valid-p) (%printf-parse-real argument) + (let* ((digits (or precision 6)) + (directive (case conversion + ((#\e #\E) "e") + ((#\g #\G) "g") + (otherwise "f"))) + (text (format nil + (concatenate 'string "~," (princ-to-string digits) directive) + number)) + (text (if (and (member conversion '(#\E #\G)) + (position #\e text)) + (substitute #\E #\e text) + text)) + (text (if (and (not (minusp number)) + (%printf-flag-p flags #\+)) + (concatenate 'string "+" text) + (if (and (not (minusp number)) + (%printf-flag-p flags #\Space)) + (concatenate 'string " " text) + text)))) + (values (%printf-pad text width (%printf-flag-p flags #\-) + (if (and (%printf-flag-p flags #\0) + (not (%printf-flag-p flags #\-))) + #\0 + #\Space)) + valid-p + nil)))) + (otherwise (values "" nil nil)))) + +(defun %printf-format-once (format-string arguments) + (let ((format-index 0) + (argument-index 0) + (argument-conversion-p nil) + (valid-p t) + (stop-p nil)) + (values + (with-output-to-string (out) + (loop while (< format-index (length format-string)) + do (let ((character (char format-string format-index))) + (if (char/= character #\%) + (if (char= character #\\) + (multiple-value-bind (replacement next-index escape-stop-p) + (%printf-read-escape format-string (1+ format-index)) + (write-string replacement out) + (setf format-index next-index + stop-p escape-stop-p) + (when stop-p + (return))) + (progn + (write-char character out) + (incf format-index))) + (progn + (incf format-index) + (if (>= format-index (length format-string)) + (progn + (setf valid-p nil) + (return)) + (if (char= (char format-string format-index) #\%) + (progn + (write-char #\% out) + (incf format-index)) + (let ((flags "")) + (loop while (and (< format-index (length format-string)) + (find (char format-string format-index) + "-+0# " + :test #'char=)) + do (setf flags + (concatenate 'string + flags + (string (char format-string format-index))) + format-index (1+ format-index))) + (let ((width-start format-index) + (precision nil)) + (loop while (and (< format-index (length format-string)) + (digit-char-p (char format-string format-index))) + do (incf format-index)) + (let ((width (when (> format-index width-start) + (parse-integer format-string + :start width-start + :end format-index)))) + (when (and (< format-index (length format-string)) + (char= (char format-string format-index) #\.)) + (incf format-index) + (let ((precision-start format-index)) + (loop while (and (< format-index (length format-string)) + (digit-char-p (char format-string format-index))) + do (incf format-index)) + (setf precision + (if (= precision-start format-index) + 0 + (parse-integer format-string + :start precision-start + :end format-index))))) + (loop while (and (< format-index (length format-string)) + (find (char format-string format-index) + "hlLjzt" + :test #'char=)) + do (incf format-index)) + (if (>= format-index (length format-string)) + (progn + (setf valid-p nil) + (return)) + (let ((conversion (char format-string format-index))) + (incf format-index) + (setf argument-conversion-p t) + (if (find conversion "sbcdiuoxXeEfFgG" + :test #'char=) + (let ((argument + (if (< argument-index (length arguments)) + (nth argument-index arguments) + ""))) + (incf argument-index) + (multiple-value-bind (text value-valid-p value-stop-p) + (%printf-format-value argument conversion flags width precision) + (write-string text out) + (unless value-valid-p + (setf valid-p nil)) + (when value-stop-p + (setf stop-p t) + (return)))) + (progn + (setf valid-p nil) + (return))))))))))))))) + argument-index + argument-conversion-p + valid-p + stop-p))) + +(define-builtin %builtin-printf (context args) (context) + (let ((arguments (if (and args (string= (first args) "--")) + (rest args) + args))) + (if (null arguments) + (values nil 0) + (let* ((format-string (first arguments)) + (remaining (rest arguments)) + (valid-p t) + (output + (with-output-to-string (out) + (loop + (multiple-value-bind (text consumed argument-conversion-p once-valid-p stop-p) + (%printf-format-once format-string remaining) + (write-string text out) + (setf valid-p (and valid-p once-valid-p) + remaining (nthcdr consumed remaining)) + (when (or stop-p + (not once-valid-p) + (null remaining) + (not argument-conversion-p)) + (return))))))) + (values output (if valid-p 0 1)))))) + (define-builtin %builtin-pwd (context args) (args) (values (format nil "~a~%" (namestring (funcall (%filesystem-fn context :cwd)))) 0)) @@ -46,16 +358,16 @@ (namestring old-cwd) (if (nshell.domain.environment:env-defined-p environment "OLDPWD") (nshell.domain.environment:env-exported-p environment "OLDPWD") - t))) - (setf environment + t)) + environment (nshell.domain.environment:env-set environment "PWD" (namestring new-cwd) (if (nshell.domain.environment:env-defined-p environment "PWD") (nshell.domain.environment:env-exported-p environment "PWD") - t))) - (setf (shell-context-environment context) environment)) + t)) + (shell-context-environment context) environment)) (values (when (and args (string= (first args) "-")) (format nil "~a~%" (namestring new-cwd))) 0)))) @@ -65,8 +377,8 @@ (defun %parse-exit-status (argument) (handler-case (values (mod (parse-integer argument :junk-allowed nil) 256) t) - (error (condition) - (values nil nil condition)))) + (error () + (values nil nil)))) (define-builtin %builtin-exit (context args) () (cond @@ -104,13 +416,7 @@ (%execute-command-by-name-in-context context command command-args) (values output (%invert-status-code code)))))) -(defun %builtin-exec (context args) - (if args - (funcall (%process-fn context :exit-process) - (funcall (%process-fn context :run-external-exec) - (first args) - (rest args))) - (%builtin-usage "exec" "exec command [args...]"))) +(defun %builtin-exec (context args) (declare (ignore context)) (if args (sb-ext:quit :unix-status (nshell.infrastructure.acl:run-external-exec (first args) (rest args))) (%builtin-usage "exec" "exec command [args...]"))) (defun %contains-usage () (%builtin-usage @@ -157,7 +463,7 @@ (error-output (values error-output 2)) ((null operands) - (values (%contains-usage) 2)) + (values (%contains-usage) 2)) (t (let* ((needle (first operands)) (values (rest operands)) diff --git a/src/application/builtin-jobs.lisp b/src/application/builtin-jobs.lisp index 6c76862..1f0958b 100644 --- a/src/application/builtin-jobs.lisp +++ b/src/application/builtin-jobs.lisp @@ -29,8 +29,7 @@ (shell-context-dispatcher context) (shell-context-process-registry context) (shell-context-terminal-fns context) - job-monitor - (shell-context-process-fns context)))) + job-monitor))) (if job (values nil 0) (values (%missing-job-output "fg" (%job-spec-label args)) 1)))) @@ -40,8 +39,7 @@ (job-id (%resolve-job-id job-monitor args :active-only-p t)) (job (bg job-id (shell-context-dispatcher context) - job-monitor - (shell-context-process-fns context)))) + job-monitor))) (if job (values nil 0) (values (%missing-job-output "bg" (%job-spec-label args)) 1)))) @@ -99,8 +97,7 @@ (type-error () nil)))) (defun %parse-positive-integer (text) (let ((value (%parse-integer-designator text))) - (when (and value (plusp value)) - value))) + (and value (plusp value) value))) (defun %find-job-id-by-pid (job-monitor pid) (let ((found nil)) (nshell.domain.job-control:monitor-map-jobs @@ -149,10 +146,7 @@ (let ((job-id (%resolve-wait-job-id job-monitor job-spec))) (multiple-value-bind (job exit-code) (if job-id - (wait-for-job job-id - process-registry - job-monitor - (shell-context-process-fns context)) + (wait-for-job job-id process-registry job-monitor) (values nil nil)) (if job (setf last-code exit-code) @@ -259,18 +253,14 @@ (string= target "-"))) (nshell.domain.job-control:monitor-resolve-job-spec job-monitor target))) -(defun %kill-one-target (context job-monitor target signal) +(defun %kill-one-target (job-monitor target signal) (let ((job-id (%resolve-kill-job-id job-monitor target))) (cond - ((and job-id - (signal-job job-id - signal - job-monitor - (shell-context-process-fns context))) + ((and job-id (signal-job job-id signal job-monitor)) t) ((let ((pid (%parse-integer-designator target))) (when pid - (funcall (%process-fn context :kill-process) pid signal) + (nshell.infrastructure.acl:kill-process pid signal) t))) (t nil)))) (defun %kill-list-output () @@ -296,7 +286,6 @@ (dolist (target targets) (handler-case (unless (%kill-one-target - context job-monitor target signal-designator) (setf code 1) (format out "kill: no such process or job: ~a~%" diff --git a/src/application/builtin-macros.lisp b/src/application/builtin-macros.lisp deleted file mode 100644 index 248ba09..0000000 --- a/src/application/builtin-macros.lisp +++ /dev/null @@ -1,8 +0,0 @@ -(in-package #:nshell.application) - -(eval-when (:compile-toplevel :load-toplevel :execute) - (defmacro define-builtin (name lambda-list ignore-variables &body body) - `(defun ,name ,lambda-list - ,@(when ignore-variables - `((declare (ignore ,@ignore-variables)))) - ,@body))) diff --git a/src/application/builtin-printf-format.lisp b/src/application/builtin-printf-format.lisp deleted file mode 100644 index 62661cb..0000000 --- a/src/application/builtin-printf-format.lisp +++ /dev/null @@ -1,208 +0,0 @@ -(in-package #:nshell.application) - -(defun %printf-pad (text width left-p &optional (pad-character #\Space)) - (let ((padding (max 0 (- (or width 0) (length text))))) - (if (zerop padding) - text - (let ((padding-text (make-string padding :initial-element pad-character))) - (if left-p - (concatenate 'string text padding-text) - (concatenate 'string padding-text text)))))) - -(defun %printf-flag-p (flags flag) - (find flag flags :test #'char=)) - -(defun %printf-read-escape (text index) - (if (>= index (length text)) - (values "\\" index nil) - (let ((character (char text index))) - (case character - (#\a (values (string (code-char 7)) (1+ index) nil)) - (#\b (values (string #\Backspace) (1+ index) nil)) - (#\c (values "" (1+ index) t)) - (#\e (values (string (code-char 27)) (1+ index) nil)) - (#\f (values (string #\Page) (1+ index) nil)) - (#\n (values (string #\Newline) (1+ index) nil)) - (#\r (values (string #\Return) (1+ index) nil)) - (#\t (values (string #\Tab) (1+ index) nil)) - (#\v (values (string (code-char 11)) (1+ index) nil)) - (#\\ (values (string #\\) (1+ index) nil)) - (#\0 - (let ((cursor (1+ index)) - (value 0) - (digits 0)) - (loop while (and (< cursor (length text)) - (< digits 3) - (find (char text cursor) "01234567" :test #'char=)) - do (setf value (+ (* value 8) - (- (char-code (char text cursor)) - (char-code #\0))) - cursor (1+ cursor) - digits (1+ digits))) - (values (string (or (code-char value) #\Null)) cursor nil))) - (otherwise (values (string character) (1+ index) nil)))))) - -(defun %printf-expand-escapes (text) - (let* ((stop nil) - (result - (with-output-to-string (out) - (loop with index = 0 - while (< index (length text)) - do (if (char= (char text index) #\\) - (multiple-value-bind (replacement next-index stop-p) - (%printf-read-escape text (1+ index)) - (write-string replacement out) - (setf index next-index - stop stop-p) - (when stop - (return))) - (progn - (write-char (char text index) out) - (incf index))))))) - (values result stop))) - -(defun %printf-parse-integer (argument) - (handler-case - (values (parse-integer argument :junk-allowed nil) t) - (error () (values 0 nil)))) - -(defun %printf-parse-real (argument) - (handler-case - (let ((*read-eval* nil)) - (multiple-value-bind (value position) - (read-from-string argument nil nil) - (if (and (realp value) - (= position (length argument))) - (values (coerce value 'double-float) t) - (values 0d0 nil)))) - (error () (values 0d0 nil)))) - -(defun %printf-integer-text (number conversion flags precision) - (let* ((negative (minusp number)) - (unsigned (if negative (abs number) number)) - (base (cond - ((or (char= conversion #\x) (char= conversion #\X)) 16) - ((char= conversion #\o) 8) - (t 10))) - (digits (case base - (16 (format nil "~x" unsigned)) - (8 (format nil "~o" unsigned)) - (otherwise (format nil "~d" unsigned)))) - (digits (cond - ((char= conversion #\X) (string-upcase digits)) - ((char= conversion #\x) (string-downcase digits)) - (t digits))) - (digits (if precision - (concatenate 'string - (make-string (max 0 (- precision (length digits))) - :initial-element #\0) - digits) - digits)) - (prefix (if (and (%printf-flag-p flags #\#) - (not (zerop unsigned))) - (cond - ((char= conversion #\x) "0x") - ((char= conversion #\X) "0X") - ((char= conversion #\o) "0") - (t "")) - "")) - (sign (cond - (negative "-") - ((%printf-flag-p flags #\+) "+") - ((%printf-flag-p flags #\Space) " ") - (t "")))) - (concatenate 'string sign prefix digits))) - -(defun %printf-truncate (text precision) - (if precision - (subseq text 0 (min precision (length text))) - text)) - -(defun %printf-format-text-value (argument flags width precision) - (%printf-pad (%printf-truncate argument precision) - width - (%printf-flag-p flags #\-))) - -(defun %printf-format-binary-value (argument flags width precision) - (multiple-value-bind (text stop-p) (%printf-expand-escapes argument) - (values (%printf-pad (%printf-truncate text precision) - width - (%printf-flag-p flags #\-)) - stop-p))) - -(defun %printf-format-character-value (argument flags width) - (%printf-pad (if (plusp (length argument)) - (string (char argument 0)) - (string #\Null)) - width - (%printf-flag-p flags #\-))) - -(defun %printf-zero-pad-p (flags precision) - (and (%printf-flag-p flags #\0) - (not (%printf-flag-p flags #\-)) - (null precision))) - -(defun %printf-format-integer-value (argument conversion flags width precision) - (multiple-value-bind (number valid-p) (%printf-parse-integer argument) - (values (%printf-pad (%printf-integer-text number conversion flags precision) - width - (%printf-flag-p flags #\-) - (if (%printf-zero-pad-p flags precision) - #\0 - #\Space)) - valid-p))) - -(defun %printf-real-text (number conversion flags precision) - (let* ((digits (or precision 6)) - (text (cond - ((or (char= conversion #\e) (char= conversion #\E)) - (format nil "~,vE" digits number)) - ((or (char= conversion #\g) (char= conversion #\G)) - (format nil "~,vG" digits number)) - (t - (format nil "~,vF" digits number)))) - (text (if (or (char= conversion #\E) (char= conversion #\G)) - (substitute #\E #\d (substitute #\E #\e text)) - text))) - (if (and (not (minusp number)) - (%printf-flag-p flags #\+)) - (concatenate 'string "+" text) - (if (and (not (minusp number)) - (%printf-flag-p flags #\Space)) - (concatenate 'string " " text) - text)))) - -(defun %printf-format-real-value (argument conversion flags width precision) - (multiple-value-bind (number valid-p) (%printf-parse-real argument) - (values (%printf-pad (%printf-real-text number conversion flags precision) - width - (%printf-flag-p flags #\-) - (if (and (%printf-flag-p flags #\0) - (not (%printf-flag-p flags #\-))) - #\0 - #\Space)) - valid-p))) - -(defun %printf-format-value (argument conversion flags width precision) - (cond - ((char= conversion #\s) - (values (%printf-format-text-value argument flags width precision) - t - nil)) - ((char= conversion #\b) - (multiple-value-bind (text stop-p) - (%printf-format-binary-value argument flags width precision) - (values text t stop-p))) - ((char= conversion #\c) - (values (%printf-format-character-value argument flags width) - t - nil)) - ((find conversion "diuoxX" :test #'char=) - (multiple-value-bind (text valid-p) - (%printf-format-integer-value argument conversion flags width precision) - (values text valid-p nil))) - ((find conversion "eEfgG" :test #'char=) - (multiple-value-bind (text valid-p) - (%printf-format-real-value argument conversion flags width precision) - (values text valid-p nil))) - (t (values "" nil nil)))) diff --git a/src/application/builtin-printf-parser.lisp b/src/application/builtin-printf-parser.lisp deleted file mode 100644 index 105891a..0000000 --- a/src/application/builtin-printf-parser.lisp +++ /dev/null @@ -1,113 +0,0 @@ -(in-package #:nshell.application) - -(defun %printf-read-flags (text index) - (let ((start index)) - (loop while (and (< index (length text)) - (find (char text index) "-+0# " :test #'char=)) - do (incf index)) - (values (subseq text start index) index))) - -(defun %printf-read-decimal (text index) - (let ((start index)) - (loop while (and (< index (length text)) - (digit-char-p (char text index))) - do (incf index)) - (values (when (> index start) - (parse-integer text :start start :end index)) - index))) - -(defun %printf-read-precision (text index) - (if (and (< index (length text)) - (char= (char text index) #\.)) - (multiple-value-bind (precision next-index) - (%printf-read-decimal text (1+ index)) - (values (or precision 0) next-index)) - (values nil index))) - -(defun %printf-skip-length (text index) - (loop while (and (< index (length text)) - (find (char text index) "hlLjzt" :test #'char=)) - do (incf index)) - index) - -(defun %printf-read-directive (text index) - (multiple-value-bind (flags index) (%printf-read-flags text index) - (multiple-value-bind (width index) (%printf-read-decimal text index) - (multiple-value-bind (precision index) (%printf-read-precision text index) - (setf index (%printf-skip-length text index)) - (if (>= index (length text)) - (values flags width precision nil index nil) - (values flags width precision (char text index) (1+ index) t)))))) - -(defun %printf-conversion-p (conversion) - (and conversion - (find conversion "sbcdiuoxXeEfFgG" :test #'char=))) - -(defun %printf-emit-directive (out format-string index arguments argument-index) - (multiple-value-bind (flags width precision conversion next-index directive-p) - (%printf-read-directive format-string index) - (cond - ((not directive-p) - (values next-index argument-index nil nil nil)) - ((not (%printf-conversion-p conversion)) - (values next-index argument-index nil nil t)) - (t - (let ((argument (if (< argument-index (length arguments)) - (nth argument-index arguments) - ""))) - (multiple-value-bind (text value-valid-p value-stop-p) - (%printf-format-value argument conversion flags width precision) - (write-string text out) - (values next-index (1+ argument-index) - value-valid-p value-stop-p t))))))) - -(defun %printf-format-once (format-string arguments) - (let ((format-index 0) - (argument-index 0) - (argument-conversion-p nil) - (valid-p t) - (stop-p nil)) - (values - (with-output-to-string (out) - (loop while (< format-index (length format-string)) - do (let ((character (char format-string format-index))) - (cond - ((char/= character #\%) - (if (char= character #\\) - (multiple-value-bind (replacement next-index escape-stop-p) - (%printf-read-escape format-string (1+ format-index)) - (write-string replacement out) - (setf format-index next-index - stop-p escape-stop-p) - (when stop-p - (return))) - (progn - (write-char character out) - (incf format-index)))) - (t - (incf format-index) - (cond - ((>= format-index (length format-string)) - (setf valid-p nil) - (return)) - ((char= (char format-string format-index) #\%) - (write-char #\% out) - (incf format-index)) - (t - (multiple-value-bind (next-index next-argument-index - directive-valid-p directive-stop-p - directive-argument-p) - (%printf-emit-directive out format-string format-index - arguments argument-index) - (setf format-index next-index - argument-index next-argument-index - argument-conversion-p - (or argument-conversion-p directive-argument-p) - valid-p (and valid-p directive-valid-p)) - (when (or (not directive-valid-p) directive-stop-p) - (setf stop-p directive-stop-p) - (return)))))))))) - argument-index - argument-conversion-p - valid-p - stop-p))) diff --git a/src/application/builtin-printf.lisp b/src/application/builtin-printf.lisp deleted file mode 100644 index 2e1782f..0000000 --- a/src/application/builtin-printf.lisp +++ /dev/null @@ -1,25 +0,0 @@ -(in-package #:nshell.application) - -(define-builtin %builtin-printf (context args) (context) - (let ((arguments (if (and args (string= (first args) "--")) - (rest args) - args))) - (if (null arguments) - (values nil 0) - (let* ((format-string (first arguments)) - (remaining (rest arguments)) - (valid-p t) - (output - (with-output-to-string (out) - (loop - (multiple-value-bind (text consumed argument-conversion-p once-valid-p stop-p) - (%printf-format-once format-string remaining) - (write-string text out) - (setf valid-p (and valid-p once-valid-p) - remaining (nthcdr consumed remaining)) - (when (or stop-p - (not once-valid-p) - (null remaining) - (not argument-conversion-p)) - (return))))))) - (values output (if valid-p 0 1)))))) diff --git a/src/application/builtin-runtime.lisp b/src/application/builtin-runtime.lisp index 77f00ae..1580d26 100644 --- a/src/application/builtin-runtime.lisp +++ b/src/application/builtin-runtime.lisp @@ -94,10 +94,7 @@ :empty-directory "")) (defun %stat-path (context path) - (handler-case - (values (funcall (%filesystem-fn context :stat) path) nil) - (error (condition) - (values nil condition)))) + (ignore-errors (funcall (%filesystem-fn context :stat) path))) (defun %path-file-p (context path) (let ((fn (%optional-filesystem-fn context :file-exists-p))) diff --git a/src/application/builtin-type-helpers.lisp b/src/application/builtin-type-helpers.lisp index 8a58434..3f86477 100644 --- a/src/application/builtin-type-helpers.lisp +++ b/src/application/builtin-type-helpers.lisp @@ -18,18 +18,14 @@ (defun %type-option-p (option) (%builtin-option-like-p option)) -(defun %type-option-prefix-p (prefix option) - (and (stringp option) - (<= (length prefix) (length option)) - (equal prefix (subseq option 0 (length prefix))))) - (defun %type-option-kind (option) (cond ((%builtin-option-p option '("-a" "--all")) :all) ((%builtin-option-p option '("-s" "--short")) :short) ((%builtin-option-p option '("-f" "--no-functions")) :no-functions) ((or (string= option "--color") - (%type-option-prefix-p "--color=" option)) + (and (>= (length option) 8) + (string= option "--color=" :end1 8 :end2 8))) :color) ((%builtin-option-p option '("-q" "--query" "--quiet")) :query) ((%builtin-option-p option '("-p" "--path")) :path) @@ -40,7 +36,8 @@ (defun %type-color-enabled-p (option) (or (string= option "--color") - (and (%type-option-prefix-p "--color=" option) + (and (>= (length option) 8) + (string= option "--color=" :end1 8 :end2 8) (let ((value (subseq option 8))) (or (string= value "always") (string= value "auto")))))) @@ -56,8 +53,8 @@ (with-output-to-string (out) (dolist (line (%string-lines text)) (write-string - (nshell.highlight:highlight->ansi - (nshell.highlight:highlight-line line) + (nshell.presentation:highlight->ansi + (nshell.presentation:highlight-line line) line (nshell.domain.configuration:default-theme)) out) diff --git a/src/application/execute-pipeline-expansion.lisp b/src/application/execute-pipeline-expansion.lisp index fe2044a..994e91a 100644 --- a/src/application/execute-pipeline-expansion.lisp +++ b/src/application/execute-pipeline-expansion.lisp @@ -22,15 +22,14 @@ arithmetic $((..)) > POSIX $(..) > bare (..) > literal character." (first forms) (let ((parts (gensym "PARTS")) (pos (gensym "POS"))) `(multiple-value-bind (,parts ,pos) ,(first forms) - (if ,pos - (values ,parts ,pos) - (%try-substitution-match ,@(rest forms)))))))) + (if ,pos + (values ,parts ,pos) + (%try-substitution-match ,@(rest forms)))))))) -(defun %make-pipeline-shell-context (process-fns) +(defun %make-pipeline-shell-context () (make-shell-context :environment (nshell.domain.environment:inject-os-environment - (nshell.domain.environment:make-default-environment)) - :process-fns process-fns)) + (nshell.domain.environment:make-default-environment)))) (defun execute-command-line (line history dispatcher) (nshell.domain.parsing:with-complete-command-line (result ast line) @@ -141,7 +140,7 @@ arithmetic $((..)) > POSIX $(..) > bare (..) > literal character." (%append-expanded-fragment-fields fields fragment-fields)))))) (defun %expand-command-name-from-fragments (command-node environment) - (nshell.domain.expansion:single-command-name-or-error + (nshell.domain.expansion::%single-command-name-or-error (nshell.domain.parsing:command-node-command command-node) (%expand-command-name-fields-from-fragments command-node environment))) @@ -207,7 +206,7 @@ arithmetic $((..)) > POSIX $(..) > bare (..) > literal character." "Run the command substitution whose opening #\( is at OPEN-PAREN. Returns (replacement next-pos) on success, or NIL when parens are empty/unbalanced. %execute-command-substitution-fields is defined later in execute-pipeline-control.lisp." - (let ((end (nshell.domain.parsing:balanced-substitution-end value open-paren))) + (let ((end (nshell.domain.parsing::%balanced-substitution-end value open-paren))) (when (and end (> end (1+ open-paren))) (values (if preserve-newlines-p (%execute-command-substitution-output diff --git a/src/application/execute-pipeline-stage-background.lisp b/src/application/execute-pipeline-stage-background.lisp index 3e95fab..43f574d 100644 --- a/src/application/execute-pipeline-stage-background.lisp +++ b/src/application/execute-pipeline-stage-background.lisp @@ -2,25 +2,16 @@ ;;; Background pipeline stage helpers. -(defun %background-call-process-operation (function &rest arguments) - (handler-case - (values (apply function arguments) nil) - (error (condition) - (values nil condition)))) - -(defun %background-process-pid (context process) +(defun %background-process-pid (process) "Return the OS PID of a background PROCESS object, or NIL if unavailable." - (%background-call-process-operation (%process-fn context :process-pid) process)) + (ignore-errors (sb-ext:process-pid process))) (defun %register-background-job (context processes command-line) "Register PROCESSES as a background job in CONTEXT's monitor and process registry. PROCESSES is a single process object (command) or a list (pipeline). - Returns the job ID, or NIL when PIDs cannot be obtained." +Returns the job ID, or NIL when PIDs cannot be obtained." (let* ((proc-list (if (listp processes) processes (list processes))) - (pids (delete nil - (mapcar (lambda (process) - (%background-process-pid context process)) - proc-list)))) + (pids (delete nil (mapcar #'%background-process-pid proc-list)))) (when pids (let ((job-id (nshell.domain.job-control:monitor-add-background-job (shell-context-job-monitor context) @@ -44,10 +35,10 @@ PROCESSES is a single process object (command) or a list (pipeline). (nshell.domain.parsing:pipeline-node-commands command)) (cond (error - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (values error 127)) (resources - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (values (%process-substitution-error "is not supported in background jobs") @@ -62,8 +53,8 @@ PROCESSES is a single process object (command) or a list (pipeline). redirect-split)) (clean-pipeline (nshell.domain.parsing:make-pipeline-node clean-commands)) - (processes (funcall (%process-fn context :spawn-pipeline-async) - clean-commands :redirects redirects)) + (processes (nshell.infrastructure.acl:spawn-pipeline-async + clean-commands :redirects redirects)) (command-line (nshell.domain.parsing:ast-node->command-line clean-pipeline))) (when processes @@ -75,10 +66,10 @@ PROCESSES is a single process object (command) or a list (pipeline). (%expand-command-node-in-context context command) (cond (error - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (values error 127)) (resources - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (values (%process-substitution-error "is not supported in background jobs") @@ -95,8 +86,8 @@ PROCESSES is a single process object (command) or a list (pipeline). (args (nshell.domain.parsing:command-node-arg-values clean-command)) (command-line (nshell.domain.parsing:ast-node->command-line clean-command)) - (process (funcall (%process-fn context :spawn-async) - cmd args :redirects redirects))) + (process (nshell.infrastructure.acl:spawn-async + cmd args :redirects redirects))) (when process (%register-background-job context process command-line)) (values nil 0)))))) diff --git a/src/application/execute-pipeline-stage-external.lisp b/src/application/execute-pipeline-stage-external.lisp index e9b6343..a51db71 100644 --- a/src/application/execute-pipeline-stage-external.lisp +++ b/src/application/execute-pipeline-stage-external.lisp @@ -1,10 +1,8 @@ (in-package #:nshell.application) -(defun %wait-external-process-for-cleanup (context process) - (handler-case - (values (funcall (%process-fn context :process-wait) process) nil) - (error (condition) - (values nil condition)))) +;; Keep the timeout variable special in this compilation unit as well so +;; SBCL does not warn when referencing the shared ACL configuration. +(declaim (special nshell.infrastructure.acl:*external-command-timeout*)) ;;; External process execution helpers for pipeline stages. @@ -71,71 +69,17 @@ (t *standard-input*)) opened-input))) -(defstruct (%external-process-stage-plan - (:constructor %make-external-process-stage-plan - (command args input-target redirect-plan preserve-fds)) - (:conc-name %external-process-stage-plan-)) - command - args - input-target - redirect-plan - preserve-fds) - -(defun %external-process-stage-plan-from - (context command-node redirects process-substitution-resources) - (let* ((command (nshell.domain.parsing:command-node-command command-node)) - (args (%line-command-args command-node)) - (wrapper-p - (and command - (nshell.domain.parsing:redirects-require-shell-wrapper-p - redirects))) - (input-target - (unless wrapper-p - (nshell.domain.parsing:redirect-input-file-target redirects))) - (source-redirect-plan (%external-process-redirect-plan-from redirects)) - (redirect-plan - (if wrapper-p - (%make-external-process-redirect-plan - nil - :supersede - nil - :supersede - :stdout - :stderr - (%external-process-redirect-plan-merge-stderr-p - source-redirect-plan)) - source-redirect-plan)) - (effective-command (if wrapper-p "sh" command)) - (effective-args - (if wrapper-p - (list* "-c" - (nshell.domain.parsing:shell-redirect-script redirects) - "nshell-fd-wrapper" - command - args) - args)) - (preserve-fds - (%process-substitution-resource-fds - context - process-substitution-resources))) - (%make-external-process-stage-plan effective-command - effective-args - input-target - redirect-plan - preserve-fds))) - -(defun %start-external-process-copiers - (context process stdout-buffer stderr-buffer) +(defun %start-external-process-copiers (process stdout-buffer stderr-buffer) (let ((stdout-thread - (funcall (%process-fn context :start-stream-copier) - (funcall (%process-fn context :process-output) process) + (nshell.infrastructure.acl::%start-stream-copier + (sb-ext:process-output process) stdout-buffer "nshell process stdout copier")) (stderr-thread nil)) (when stderr-buffer (setf stderr-thread - (funcall (%process-fn context :start-stream-copier) - (funcall (%process-fn context :process-error) process) + (nshell.infrastructure.acl::%start-stream-copier + (sb-ext:process-error process) stderr-buffer "nshell process stderr copier"))) (remove nil (list stdout-thread stderr-thread)))) @@ -198,115 +142,109 @@ nil) (values returned-output exit-code))))) -(defun %wait-for-external-process - (context process stdout-buffer stderr-buffer redirect-plan command) - (let ((timeout-seconds - (funcall (%process-fn context :external-command-timeout)))) - (unwind-protect - (funcall - (%process-fn context :wait-process-with-copiers) - process - (%start-external-process-copiers - context - process - stdout-buffer - stderr-buffer) - timeout-seconds - (lambda () - (%finish-external-process-output - stdout-buffer - stderr-buffer - redirect-plan - (funcall (%process-fn context :process-exit-status-code) - process))) - (lambda () - (format *error-output* - "nshell: ~a: timed out after ~a seconds~%" - command - timeout-seconds) - (%finish-external-process-output - stdout-buffer - stderr-buffer - redirect-plan - 124))) - (when (and process - (funcall (%process-fn context :process-alive-p) process)) - (%wait-external-process-for-cleanup context process))))) - -(defmacro %with-process-substitution-lifecycle - ((context resources process-started) &body body) - `(unwind-protect - (progn ,@body) - (if ,process-started - (%finish-process-substitution-resources ,context ,resources) - (%abort-process-substitution-resources ,context ,resources)))) - -(defun %execute-external-process - (context stage-plan stdin command &optional process-substitution-resources) - (let ((process-started nil)) - (%with-process-substitution-lifecycle - (context process-substitution-resources process-started) - (let* ((stdout-buffer (make-string-output-stream)) - ;; Asked once and reused: 2>&1 decides both whether a separate - ;; stderr buffer exists and what :error is. - (redirect-plan - (%external-process-stage-plan-redirect-plan stage-plan)) - (merge-stderr-p - (%external-process-redirect-plan-merge-stderr-p - redirect-plan)) - (stderr-buffer - (unless merge-stderr-p - (make-string-output-stream))) - (process - (funcall (%process-fn context :run-program) - (%external-process-stage-plan-command stage-plan) - (%external-process-stage-plan-args stage-plan) - :input stdin - :output :stream - :error (if merge-stderr-p :output :stream) - :wait nil - :search t - :preserve-fds - (%external-process-stage-plan-preserve-fds stage-plan)))) - (setf process-started t) - (%release-process-substitution-resources - context - process-substitution-resources) - (%wait-for-external-process - context - process - stdout-buffer - stderr-buffer - redirect-plan - command))))) - (defun %execute-external-pipeline-stage - (context command-node input redirects &optional process-substitution-resources) + (command-node input redirects &optional process-substitution-resources) "Execute COMMAND-NODE as an external process with optional INPUT string. Applies REDIRECTS and returns (output exit-code)." (let* ((command (nshell.domain.parsing:command-node-command command-node)) - (stage-plan - (%external-process-stage-plan-from - context - command-node - redirects - process-substitution-resources))) + (args (%line-command-args command-node)) + (wrapper-p + (and command + (nshell.domain.parsing:redirects-require-shell-wrapper-p + redirects))) + (input-target + (unless wrapper-p + (nshell.domain.parsing:redirect-input-file-target redirects))) + (source-redirect-plan (%external-process-redirect-plan-from redirects)) + ;; The child wrapper applies file redirects itself. Keep only the + ;; stream topology needed to route data that remains on the parent's + ;; captured stdout/stderr pipes. + (redirect-plan + (if wrapper-p + (%make-external-process-redirect-plan + nil + :supersede + nil + :supersede + :stdout + :stderr + (%external-process-redirect-plan-merge-stderr-p + source-redirect-plan)) + source-redirect-plan)) + (effective-command (if wrapper-p "sh" command)) + (effective-args + (if wrapper-p + (list* "-c" + (nshell.domain.parsing:shell-redirect-script redirects) + "nshell-fd-wrapper" + command + args) + args)) + (preserve-fds + (%process-substitution-resource-fds + process-substitution-resources)) + (process-started nil)) (handler-case (multiple-value-bind (stdin opened-input) - (%external-process-input-stream - (%external-process-stage-plan-input-target stage-plan) - input) + (%external-process-input-stream input-target input) (unwind-protect - (%execute-external-process - context - stage-plan - stdin - command - process-substitution-resources) - (when opened-input - (close opened-input)))) + (unwind-protect + (let* ((stdout-buffer (make-string-output-stream)) + ;; Asked once and reused: 2>&1 decides both whether a + ;; separate stderr buffer exists and what :error is. + (merge-stderr-p + (%external-process-redirect-plan-merge-stderr-p + redirect-plan)) + (stderr-buffer + (unless merge-stderr-p + (make-string-output-stream))) + (process (sb-ext:run-program + effective-command + effective-args + :input stdin + :output :stream + :error (if merge-stderr-p + :output + :stream) + :wait nil + :search t + :preserve-fds preserve-fds))) + (setf process-started t) + (%release-process-substitution-resources + process-substitution-resources) + (unwind-protect + (nshell.infrastructure.acl::%wait-process-with-copiers + process + (%start-external-process-copiers process + stdout-buffer + stderr-buffer) + nshell.infrastructure.acl:*external-command-timeout* + (lambda () + (%finish-external-process-output + stdout-buffer + stderr-buffer + redirect-plan + (nshell.infrastructure.acl:process-exit-status-code + process))) + (lambda () + (format *error-output* + "nshell: ~a: timed out after ~a seconds~%" + command + nshell.infrastructure.acl:*external-command-timeout*) + (%finish-external-process-output + stdout-buffer + stderr-buffer + redirect-plan + 124))) + (when (and process (sb-ext:process-alive-p process)) + (ignore-errors (sb-ext:process-wait process))))) + (when opened-input + (close opened-input))) + (if process-started + (%finish-process-substitution-resources + process-substitution-resources) + (%abort-process-substitution-resources + process-substitution-resources)))) (error (condition) - (%abort-process-substitution-resources - context - process-substitution-resources) + (%abort-process-substitution-resources process-substitution-resources) (values (format nil "nshell: ~a: ~a~%" command condition) 127))))) diff --git a/src/application/execute-pipeline-stage.lisp b/src/application/execute-pipeline-stage.lisp index 2f709c5..764f5ad 100644 --- a/src/application/execute-pipeline-stage.lisp +++ b/src/application/execute-pipeline-stage.lisp @@ -1,10 +1,9 @@ (in-package #:nshell.application) -(defun %call-process-cleanup (function &rest arguments) - (handler-case - (values (apply function arguments) nil) - (error (condition) - (values nil condition)))) +(declaim (notinline nshell.infrastructure.acl:process-substitution-resource-path + nshell.infrastructure.acl:process-substitution-resource-fd)) + +(declaim (special nshell.infrastructure.acl:*external-command-timeout*)) ;;; Pipeline stage execution and command expansion. ;;; execute-ast-in-context (defined in execute-pipeline-control.lisp) is @@ -36,31 +35,25 @@ (t nil))) -(defun %release-process-substitution-resources (context resources) +(defun %release-process-substitution-resources (resources) (dolist (resource resources) - (%call-process-cleanup - (%process-fn context :release-process-substitution-fd) - resource))) + (ignore-errors + (nshell.infrastructure.acl:release-process-substitution-fd resource)))) -(defun %finish-process-substitution-resources (context resources) +(defun %finish-process-substitution-resources (resources) (dolist (resource resources) - (%call-process-cleanup - (%process-fn context :wait-process-substitution) - resource) - (%call-process-cleanup - (%process-fn context :release-process-substitution-fd) - resource))) + (ignore-errors + (nshell.infrastructure.acl:wait-process-substitution resource)) + (ignore-errors + (nshell.infrastructure.acl:release-process-substitution-fd resource)))) -(defun %abort-process-substitution-resources (context resources) +(defun %abort-process-substitution-resources (resources) (dolist (resource resources) - (%call-process-cleanup - (%process-fn context :close-process-substitution) - resource))) + (ignore-errors + (nshell.infrastructure.acl:close-process-substitution resource)))) -(defun %process-substitution-resource-fds (context resources) - (mapcar (lambda (resource) - (funcall (%process-fn context :process-substitution-resource-fd) - resource)) +(defun %process-substitution-resource-fds (resources) + (mapcar #'nshell.infrastructure.acl:process-substitution-resource-fd resources)) (defun %materialize-process-substitution-in-context (context value) @@ -89,7 +82,7 @@ context commands) (when nested-resources - (%abort-process-substitution-resources context nested-resources) + (%abort-process-substitution-resources nested-resources) (return-from %materialize-process-substitution-in-context (values @@ -116,18 +109,13 @@ (nshell.domain.parsing:command-list-redirect-split-result-redirects redirect-split))) (handler-case (let ((resource - (funcall (%process-fn - context - :spawn-process-substitution) - direction - clean-commands - :redirects - redirects))) + (nshell.infrastructure.acl:spawn-process-substitution + direction + clean-commands + :redirects + redirects))) (values - (funcall - (%process-fn - context - :process-substitution-resource-path) + (nshell.infrastructure.acl:process-substitution-resource-path resource) resource nil)) @@ -157,7 +145,7 @@ (multiple-value-bind (path resource error) (%materialize-process-substitution-in-context context value) (when error - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (return-from %expand-command-args-in-context (values nil nil error))) (setf args @@ -175,7 +163,7 @@ (not (null (member value '("<<" "<<-") :test #'string=)))))) (values args (nreverse resources) nil)) (nshell.domain.expansion:parameter-expansion-error (condition) - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (values nil nil (format nil "nshell: ~a~%" condition)))))) @@ -215,7 +203,7 @@ command name expands to zero or multiple fields (ambiguous)." (%expand-command-node-in-context context command) (if error (progn - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (return-from %expand-command-nodes-in-context (values nil error nil))) (progn @@ -255,7 +243,7 @@ command name expands to zero or multiple fields (ambiguous)." (with-input-from-string (*standard-input* input) (%execute-clean-command-node-in-context context command-node redirects)) (%execute-clean-command-node-in-context context command-node redirects)) - (%execute-external-pipeline-stage context command-node input redirects))) + (%execute-external-pipeline-stage command-node input redirects))) (defun %execute-source-pipeline-in-context (context commands redirects) (let ((input nil) @@ -302,7 +290,7 @@ command name expands to zero or multiple fields (ambiguous)." (%restore-context-redirects context))))) (defun %execute-os-pipeline-with-process-substitutions - (context clean-commands redirects resources &optional (pipefail-p nil)) + (clean-commands redirects resources &optional (pipefail-p nil)) (let ((spawned-p nil)) (unwind-protect (handler-case @@ -311,17 +299,16 @@ command name expands to zero or multiple fields (ambiguous)." (values (with-output-to-string (*standard-output*) (multiple-value-bind (status statuses) - (funcall (%process-fn context :spawn-pipeline) + (nshell.infrastructure.acl:spawn-pipeline clean-commands :redirects redirects :pipefail-p pipefail-p :preserve-fds - (%process-substitution-resource-fds context resources) + (%process-substitution-resource-fds resources) :after-spawn (lambda () (setf spawned-p t) (%release-process-substitution-resources - context resources))) (setf exit-code (or status 0) pipeline-statuses (or statuses @@ -331,14 +318,14 @@ command name expands to zero or multiple fields (ambiguous)." (error (condition) (values (format nil "nshell: ~a~%" condition) 127 (list 127)))) (if spawned-p - (%finish-process-substitution-resources context resources) - (%abort-process-substitution-resources context resources))))) + (%finish-process-substitution-resources resources) + (%abort-process-substitution-resources resources))))) (defun execute-command-node-in-context (context command-node) (multiple-value-bind (expanded error resources) (%expand-command-node-in-context context command-node) (when error - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (return-from execute-command-node-in-context (values error 127))) (let* ((redirect-split (%extract-command-redirects expanded)) (clean-command @@ -350,25 +337,18 @@ command name expands to zero or multiple fields (ambiguous)." (if resources (if (%shell-internal-command-p context clean-command) (progn - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (values (%process-substitution-error "requires an external command") 127)) (%execute-external-pipeline-stage - context - clean-command - nil - redirects - resources)) + clean-command nil redirects resources)) (if (and (not (%shell-internal-command-p context clean-command)) (nshell.domain.parsing:redirects-require-shell-wrapper-p redirects)) (%execute-external-pipeline-stage - context - clean-command - nil - redirects) + clean-command nil redirects) (%execute-clean-command-node-in-context context clean-command redirects)))))) @@ -379,7 +359,7 @@ command name expands to zero or multiple fields (ambiguous)." (multiple-value-bind (expanded-commands error resources) (%expand-command-nodes-in-context context commands) (when error - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (return-from execute-pipeline-node-in-context (values error 127))) (let* ((redirect-split (%extract-pipeline-redirects expanded-commands)) (clean-commands @@ -393,15 +373,14 @@ command name expands to zero or multiple fields (ambiguous)." (some (lambda (cmd) (%shell-internal-command-p context cmd)) clean-commands)) (progn - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (values (%process-substitution-error "is not supported for internal or CPS pipelines") 127)) (multiple-value-bind (output exit-code statuses) - (%execute-os-pipeline-with-process-substitutions - context - clean-commands redirects resources + (%execute-os-pipeline-with-process-substitutions + clean-commands redirects resources (shell-context-pipefail-p context)) (%record-pipeline-statuses context statuses) (values output exit-code))) @@ -417,7 +396,7 @@ command name expands to zero or multiple fields (ambiguous)." (setf output (with-output-to-string (*standard-output*) (multiple-value-bind (status statuses) - (funcall (%process-fn context :spawn-pipeline) + (nshell.infrastructure.acl:spawn-pipeline clean-commands :redirects redirects :pipefail-p @@ -430,16 +409,16 @@ command name expands to zero or multiple fields (ambiguous)." ;; -- Public pipeline API (OS-level) ------------------------------------------- -(defun execute-pipeline (pipeline-ast process-fns) +(defun execute-pipeline (pipeline-ast) "Execute a pipeline AST using OS-level pipes. Returns the last process exit code." (let ((commands (if (nshell.domain.parsing:pipeline-node-p pipeline-ast) (nshell.domain.parsing:pipeline-node-commands pipeline-ast) (list pipeline-ast)))) - (let ((context (%make-pipeline-shell-context process-fns))) + (let ((context (%make-pipeline-shell-context))) (multiple-value-bind (expanded-commands error resources) (%expand-command-nodes-in-context context commands) (when error - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (write-string error *error-output*) (return-from execute-pipeline 127)) (let* ((redirect-split (%extract-pipeline-redirects expanded-commands)) @@ -453,7 +432,7 @@ command name expands to zero or multiple fields (ambiguous)." (some (lambda (cmd) (%shell-internal-command-p context cmd)) clean-commands)) (progn - (%abort-process-substitution-resources context resources) + (%abort-process-substitution-resources resources) (write-string (%process-substitution-error "is not supported for internal commands") @@ -462,20 +441,19 @@ command name expands to zero or multiple fields (ambiguous)." (if resources (nth-value 1 - (%execute-os-pipeline-with-process-substitutions - context - clean-commands redirects resources + (%execute-os-pipeline-with-process-substitutions + clean-commands redirects resources (shell-context-pipefail-p context))) - (funcall (%process-fn context :spawn-pipeline) + (nshell.infrastructure.acl:spawn-pipeline clean-commands :redirects redirects :pipefail-p (shell-context-pipefail-p context))))))))) -(defun execute-pipeline-use-case (pipeline dispatcher process-fns) +(defun execute-pipeline-use-case (pipeline dispatcher) (when dispatcher (publish-event dispatcher (nshell.domain.events:make-pipeline-started-event pipeline nil))) - (let ((exit-code (or (execute-pipeline pipeline process-fns) 0))) + (let ((exit-code (or (execute-pipeline pipeline) 0))) (when dispatcher (publish-event dispatcher (nshell.domain.events:make-pipeline-completed-event pipeline exit-code))) diff --git a/src/application/manage-job.lisp b/src/application/manage-job.lisp index ea71de0..ddbe45c 100644 --- a/src/application/manage-job.lisp +++ b/src/application/manage-job.lisp @@ -1,8 +1,10 @@ (in-package #:nshell.application) +(eval-when (:compile-toplevel :load-toplevel :execute) + (require :sb-posix)) + (defvar *job-monitor* (nshell.domain.job-control:make-job-monitor)) -(defvar *job-process-fns* nil) -(defvar *shell-pgid* 0) +(defvar *shell-pgid* (sb-posix:getpid)) (defvar *foreground-job-pgid* nil) (define-value-struct job-listing @@ -25,95 +27,51 @@ state status-code) -(defun %optional-job-process-fn (key) - (getf *job-process-fns* key)) - -(defun %job-process-fn (key) - (or (%optional-job-process-fn key) - (error "Missing job process capability ~s" key))) - -(defun %call-optional-job-process-fn (fn &rest args) - "Call optional process capability FN with ARGS. -Returns the capability result as the primary value and any failure condition -as the secondary value, so unsupported terminal operations remain observable -without aborting job-control cleanup." - (handler-case - (values (apply fn args) nil) - (error (condition) - (values nil condition)))) - (defun %set-acl-foreground-pgid (pgid) - (let ((fn (%optional-job-process-fn :set-foreground-pgid-state))) - (when fn - (funcall fn pgid))) + (let ((symbol (find-symbol "*FOREGROUND-PGID*" "NSHELL.INFRASTRUCTURE.ACL"))) + (when symbol + (setf (symbol-value symbol) (or pgid 0)))) (values)) (defun %continue-process-group (pgid) - (let ((fn (%optional-job-process-fn :continue-process-group))) - (when fn - (funcall fn pgid)))) - -(defun %get-foreground-pgroup () - (let ((fn (%optional-job-process-fn :get-foreground-pgroup))) - (when fn - (%call-optional-job-process-fn fn)))) - -(defun %set-foreground-pgroup (pgid) - (let ((fn (%optional-job-process-fn :set-foreground-pgroup))) - (when fn - (%call-optional-job-process-fn fn pgid)))) - -(defun %kill-process (pid signal) - (let ((fn (%optional-job-process-fn :kill-process))) - (when fn - (funcall fn pid signal)))) - -(defun %shell-process-group-id () - (or (and (integerp *shell-pgid*) - (plusp *shell-pgid*) - *shell-pgid*) - (let ((fn (%optional-job-process-fn :current-process-id))) - (and fn (%call-optional-job-process-fn fn))) - 0)) + (sb-posix:kill (- pgid) sb-unix:sigcont)) (defun fg (job-id &optional dispatcher process-registry terminal-fns - (job-monitor *job-monitor*) process-fns) + (job-monitor *job-monitor*)) "Move JOB-ID to the foreground, wait for it, then restore the shell PGID." (declare (ignore process-registry terminal-fns)) - (let ((*job-process-fns* (or process-fns *job-process-fns*))) - (let ((job (%require-job job-id job-monitor))) - (when job - (let ((pgid (nshell.domain.execution:job-control-pgid job))) - (when pgid - (setf *foreground-job-pgid* pgid) - (unwind-protect - (progn - (%set-acl-foreground-pgid pgid) - (%continue-process-group pgid) - (nshell.domain.job-control:foreground-job job-monitor job-id) - (when dispatcher - (publish-event dispatcher - (nshell.domain.events:make-job-continued-event job-id))) - (%with-terminal-foreground-pgroup - pgid - (lambda () (%wait-job-pgid job job-id job-monitor)))) - (setf *foreground-job-pgid* nil) - (%set-acl-foreground-pgid nil))) - job))))) + (let ((job (%require-job job-id job-monitor))) + (when job + (let ((pgid (nshell.domain.execution:job-control-pgid job))) + (when pgid + (setf *foreground-job-pgid* pgid) + (unwind-protect + (progn + (%set-acl-foreground-pgid pgid) + (%continue-process-group pgid) + (nshell.domain.job-control:foreground-job job-monitor job-id) + (when dispatcher + (publish-event dispatcher + (nshell.domain.events:make-job-continued-event job-id))) + (%with-terminal-foreground-pgroup + pgid + (lambda () (%wait-job-pgid job job-id job-monitor)))) + (setf *foreground-job-pgid* nil) + (%set-acl-foreground-pgid nil))) + job)))) -(defun bg (job-id &optional dispatcher (job-monitor *job-monitor*) process-fns) +(defun bg (job-id &optional dispatcher (job-monitor *job-monitor*)) "Continue JOB-ID in the background." - (let ((*job-process-fns* (or process-fns *job-process-fns*))) - (let ((job (%require-job job-id job-monitor))) - (when job - (let ((pgid (nshell.domain.execution:job-control-pgid job))) - (when pgid - (%continue-process-group pgid)) - (nshell.domain.job-control:background-job job-monitor job-id) - (when dispatcher - (publish-event dispatcher - (nshell.domain.events:make-job-continued-event job-id))) - job))))) + (let ((job (%require-job job-id job-monitor))) + (when job + (let ((pgid (nshell.domain.execution:job-control-pgid job))) + (when pgid + (%continue-process-group pgid)) + (nshell.domain.job-control:background-job job-monitor job-id) + (when dispatcher + (publish-event dispatcher + (nshell.domain.events:make-job-continued-event job-id))) + job)))) (defun jobs (&optional (job-monitor *job-monitor*)) "Return current job listings without writing to the terminal." @@ -140,10 +98,10 @@ without aborting job-control cleanup." (defun %foreground-signal-target-pgid () (let ((pgid (or *foreground-job-pgid* - (%get-foreground-pgroup)))) + (ignore-errors (nshell.infrastructure.acl:get-foreground-pgroup))))) (when (and pgid (nshell.domain.execution:valid-process-group-id-p pgid) - (/= pgid (%shell-process-group-id))) + (/= pgid *shell-pgid*)) pgid))) (defun %job-command-string (job) @@ -159,12 +117,13 @@ without aborting job-control cleanup." (otherwise "Unknown"))) (defun %with-terminal-foreground-pgroup (pgid thunk) - (let ((previous (%get-foreground-pgroup))) + (let ((previous (ignore-errors (nshell.infrastructure.acl:get-foreground-pgroup)))) (unwind-protect (progn - (%set-foreground-pgroup pgid) + (ignore-errors (nshell.infrastructure.acl:set-foreground-pgroup pgid)) (funcall thunk)) - (%set-foreground-pgroup (or previous (%shell-process-group-id)))))) + (ignore-errors + (nshell.infrastructure.acl:set-foreground-pgroup (or previous *shell-pgid*)))))) (defun %classify-job-wait-status (pid status &key stopped-p exited-p exit-status @@ -182,22 +141,30 @@ without aborting job-control cleanup." (defun %classify-job-wait-error (errno) (cond - ((member errno '(:echild :no-child)) + ((= errno sb-posix:echild) (%make-job-wait-event nil :no-child nil)) - ((member errno '(:eintr :interrupted)) + ((= errno sb-posix:eintr) (%make-job-wait-event nil :interrupted nil)) (t nil))) (defun %wait-job-pgid-event (pgid) - (multiple-value-bind (pid state detail) - (funcall (%job-process-fn :wait-job) (- pgid) :untraced t) - (case state - (:stopped (%make-job-wait-event pid :stopped nil)) - (:exited (%make-job-wait-event pid :exited detail)) - (:signaled (%make-job-wait-event pid :signaled (+ 128 detail))) - (:no-child (%make-job-wait-event nil :no-child nil)) - (:continued (%make-job-wait-event pid :continued nil)) - (otherwise (%make-job-wait-event pid :unknown nil))))) + (handler-case + (multiple-value-bind (pid status) + (sb-posix:waitpid (- pgid) sb-posix:wuntraced) + (%classify-job-wait-status + pid status + :stopped-p #'sb-posix:wifstopped + :exited-p #'sb-posix:wifexited + :exit-status #'sb-posix:wexitstatus + :signaled-p #'sb-posix:wifsignaled + :term-signal #'sb-posix:wtermsig)) + (sb-posix:syscall-error (condition) + (let ((event + (%classify-job-wait-error + (sb-posix:syscall-errno condition)))) + (if event + event + (error condition)))))) (defun %wait-job-pgid (job job-id job-monitor) (let* ((pgid (nshell.domain.execution:job-control-pgid job)) @@ -245,77 +212,79 @@ without aborting job-control cleanup." (defun %require-job (job-id &optional (job-monitor *job-monitor*)) (nshell.domain.job-control:monitor-find-job job-monitor job-id)) -(defun signal-job (job-id signal &optional (job-monitor *job-monitor*) process-fns) +(defun signal-job (job-id signal &optional (job-monitor *job-monitor*)) "Send SIGNAL to JOB-ID and synchronize the monitor after a successful signal." - (let ((*job-process-fns* (or process-fns *job-process-fns*))) - (labels ((stop-signal-p (candidate) - (member candidate '(:sigstop :sigtstp))) - (continue-signal-p (candidate) - (eq candidate :sigcont)) - (record-signal-state () - (let ((current-job - (nshell.domain.job-control:monitor-find-job - job-monitor job-id))) - (when (and current-job - (not (nshell.domain.execution:job-completed-p - current-job))) - (cond - ((stop-signal-p signal) - (nshell.domain.job-control:suspend-job - job-monitor job-id nil)) - ((and (continue-signal-p signal) - (nshell.domain.execution:job-stopped-p current-job)) - (nshell.domain.job-control:background-job - job-monitor job-id))))))) - (let ((job (%require-job job-id job-monitor))) - (when job - (let ((pgid (nshell.domain.execution:job-control-pgid job)) - (pids (nshell.domain.execution:job-known-pids job)) - (signaled-p nil)) - (if pgid - (progn - (%kill-process (- pgid) signal) - (setf signaled-p t)) - (dolist (pid pids) - (%kill-process pid signal) - (setf signaled-p t))) - (when signaled-p - (record-signal-state)) - job)))))) + (labels ((stop-signal-p (candidate) + (or (member candidate (list :sigstop :sigtstp)) + (and (integerp candidate) + (member candidate + (list sb-unix:sigstop sb-unix:sigtstp))))) + (continue-signal-p (candidate) + (or (eq candidate :sigcont) + (and (integerp candidate) + (= candidate sb-unix:sigcont)))) + (record-signal-state () + (let ((current-job + (nshell.domain.job-control:monitor-find-job + job-monitor job-id))) + (when (and current-job + (not (nshell.domain.execution:job-completed-p + current-job))) + (cond + ((stop-signal-p signal) + (nshell.domain.job-control:suspend-job + job-monitor job-id nil)) + ((and (continue-signal-p signal) + (nshell.domain.execution:job-stopped-p current-job)) + (nshell.domain.job-control:background-job + job-monitor job-id))))))) + (let ((job (%require-job job-id job-monitor))) + (when job + (let ((pgid (nshell.domain.execution:job-control-pgid job)) + (pids (nshell.domain.execution:job-known-pids job)) + (signaled-p nil)) + (if pgid + (progn + (nshell.infrastructure.acl:kill-process (- pgid) signal) + (setf signaled-p t)) + (dolist (pid pids) + (nshell.infrastructure.acl:kill-process pid signal) + (setf signaled-p t))) + (when signaled-p + (record-signal-state)) + job))))) (defun wait-for-job (job-id process-registry - &optional (job-monitor *job-monitor*) process-fns) + &optional (job-monitor *job-monitor*)) "Wait for JOB-ID using the registered SBCL process objects. The process registry is shared with the background reaper. Waiting through the same process objects avoids a second waitpid consumer and preserves the exit status when the reaper has already completed the domain job." - (let ((*job-process-fns* (or process-fns *job-process-fns*))) - (let ((job (%require-job job-id job-monitor))) - (cond - ((null job) - (values nil nil)) - ((nshell.domain.execution:job-completed-p job) - (remhash job-id process-registry) - (values job - (or (nshell.domain.execution:job-exit-code job) - 0))) - (t - (let ((processes (%job-process-list (gethash job-id process-registry)))) - (if (null processes) - (values nil nil) - (progn - (%wait-job-processes processes) - (let ((exit-code (%job-process-exit-code job processes))) - (nshell.domain.job-control:complete-job - job-monitor job-id exit-code) - (remhash job-id process-registry) - (values job exit-code)))))))))) + (let ((job (%require-job job-id job-monitor))) + (cond + ((null job) + (values nil nil)) + ((nshell.domain.execution:job-completed-p job) + (remhash job-id process-registry) + (values job + (or (nshell.domain.execution:job-exit-code job) + 0))) + (t + (let ((processes (%job-process-list (gethash job-id process-registry)))) + (if (null processes) + (values nil nil) + (progn + (%wait-job-processes processes) + (let ((exit-code (%job-process-exit-code job processes))) + (nshell.domain.job-control:complete-job + job-monitor job-id exit-code) + (remhash job-id process-registry) + (values job exit-code))))))))) (defun %job-process-exit-code (job processes) (let ((statuses (mapcar (lambda (process) - (let ((fn (%optional-job-process-fn :process-exit-status-code))) - (and fn (funcall fn process)))) + (nshell.infrastructure.acl:process-exit-status-code process)) (remove nil processes)))) (if (nshell.domain.execution:job-pipefail-p job) (or (find-if (lambda (status) @@ -326,10 +295,9 @@ exit status when the reaper has already completed the domain job." (nshell.domain.execution:job-exit-code job) 0)))) (defun %wait-job-processes (processes) - (let ((fn (%optional-job-process-fn :process-wait))) - (dolist (process processes) - (when (and fn process) - (funcall fn process)))) + (dolist (process processes) + (when process + (sb-ext:process-wait process))) processes) (defun %job-process-list (entry) (cond diff --git a/src/domain/expansion/fields.lisp b/src/domain/expansion/fields.lisp index 63d65ae..395dc21 100644 --- a/src/domain/expansion/fields.lisp +++ b/src/domain/expansion/fields.lisp @@ -96,11 +96,11 @@ returning the (possibly multiple) resulting fields." "Dispatch on STYLE and evaluate the matching form." (let ((style-var (gensym "STYLE"))) `(let ((,style-var ,style)) - (case ,style-var - ((nil) ,unquoted-form) - (:single ,single-form) - (:double ,double-form) - (t (error "Invalid quote style ~S" ,style-var)))))) + (case ,style-var + ((nil) ,unquoted-form) + (:single ,single-form) + (:double ,double-form) + (t (error "Invalid quote style ~S" ,style-var)))))) (defun expand-double-quoted (input env) "Expand INPUT as the contents of a double-quoted string. @@ -124,8 +124,8 @@ single string. Command substitution is applied by the caller before this." (boundaries nil :type list) (start nil :type (or null integer))) -(defun whitespace-field-separator-p (character) - (member character '(#\Space #\Tab #\Newline) :test #'char=)) +(defun whitespace-field-separator-p (char) + (member char '(#\Space #\Tab #\Newline) :test #'char=)) (defun whitespace-field-scanner-start-field (scanner index) (unless (whitespace-field-scanner-start scanner) @@ -200,13 +200,13 @@ single string. Command substitution is applied by the caller before this." (values nil (%command-name-candidate-error candidate (length fields)))))) -(defun single-command-name-or-error (text fields) +(defun %single-command-name-or-error (text fields) "Return one command name from FIELDS or the canonical ambiguity error." (%resolve-command-name-candidate (%make-command-name-candidate text fields))) (defun expand-command-name-by-quote-style (text style env) "Expand a command name and return either one field or an ambiguity error." - (single-command-name-or-error + (%single-command-name-or-error text (expand-command-name-fields-by-quote-style text style env))) diff --git a/src/domain/parsing/tokenizer-data.lisp b/src/domain/parsing/tokenizer-data.lisp index d89c65d..661768d 100644 --- a/src/domain/parsing/tokenizer-data.lisp +++ b/src/domain/parsing/tokenizer-data.lisp @@ -184,10 +184,7 @@ fragments) (setf (tokenizer-state-pos state) (%token-extent-end extent)))) -(defun balanced-substitution-end (input start) - "Return the closing-parenthesis index for the substitution at START. - -Quotes and escaped characters do not affect the parenthesis depth." +(defun %balanced-substitution-end (input start) (let ((depth 0) (quote nil) (escaped nil)) @@ -211,4 +208,4 @@ Quotes and escaped characters do not affect the parenthesis depth." (return index))))))) (defun %tokenizer-balanced-substitution-end (state start) - (balanced-substitution-end (tokenizer-state-input state) start)) + (%balanced-substitution-end (tokenizer-state-input state) start)) diff --git a/src/infrastructure/acl/process-capabilities.lisp b/src/infrastructure/acl/process-capabilities.lisp deleted file mode 100644 index 3e256c0..0000000 --- a/src/infrastructure/acl/process-capabilities.lisp +++ /dev/null @@ -1,104 +0,0 @@ -(in-package #:nshell.infrastructure.acl) - -;;; The application receives this table at the composition boundary. Keeping -;;; the SBCL and ACL details here lets execution depend on operations, not on -;;; a particular process implementation. - -(defun make-process-fns () - "Return the concrete process capability table for a live nshell session." - (list - :spawn-pipeline - (lambda (&rest args) - (apply #'spawn-pipeline args)) - :spawn-pipeline-async - (lambda (&rest args) - (apply #'spawn-pipeline-async args)) - :spawn-process-substitution - (lambda (&rest args) - (apply #'spawn-process-substitution args)) - ;; NOTINLINE: these two are defstruct accessors, which SBCL inlines by - ;; default. An inlined call bypasses the function cell entirely, so a - ;; test's WITH-TEMPORARY-FUNCTION mock on the accessor's symbol would be - ;; silently ignored without this declaration. - :process-substitution-resource-path - (lambda (resource) - (locally (declare (notinline process-substitution-resource-path)) - (process-substitution-resource-path resource))) - :process-substitution-resource-fd - (lambda (resource) - (locally (declare (notinline process-substitution-resource-fd)) - (process-substitution-resource-fd resource))) - :release-process-substitution-fd - (lambda (resource) - (release-process-substitution-fd resource)) - :wait-process-substitution - (lambda (resource) - (wait-process-substitution resource)) - :close-process-substitution - (lambda (resource) - (close-process-substitution resource)) - :spawn-async - (lambda (&rest args) - (apply #'spawn-async args)) - :run-program - (lambda (program args &rest options) - (apply #'sb-ext:run-program program args options)) - :process-output - (lambda (process) - (sb-ext:process-output process)) - :process-error - (lambda (process) - (sb-ext:process-error process)) - :process-alive-p - (lambda (process) - (sb-ext:process-alive-p process)) - :process-wait - (lambda (process) - (sb-ext:process-wait process)) - :process-pid - (lambda (process) - (sb-ext:process-pid process)) - :start-stream-copier - (lambda (&rest args) - (apply #'start-stream-copier args)) - :wait-process-with-copiers - (lambda (&rest args) - (apply #'wait-process-with-copiers args)) - :process-exit-status-code - (lambda (process) - (process-exit-status-code process)) - :external-command-timeout - (lambda () - *external-command-timeout*) - :run-external - (lambda (&rest args) - (apply #'run-external args)) - :run-external-capture - (lambda (&rest args) - (apply #'run-external-capture args)) - :run-external-exec - (lambda (&rest args) - (apply #'run-external-exec args)) - :exit-process - (lambda (status) - (sb-ext:quit :unix-status status)) - :kill-process - (lambda (&rest args) - (apply #'kill-process args)) - :continue-process-group - (lambda (pgid) - (kill-process (- pgid) :sigcont)) - :get-foreground-pgroup - (lambda () - (get-foreground-pgroup)) - :set-foreground-pgroup - (lambda (pgid) - (set-foreground-pgroup pgid)) - :set-foreground-pgid-state - (lambda (pgid) - (setf *foreground-pgid* (or pgid 0))) - :wait-job - (lambda (&rest args) - (apply #'wait-job args)) - :current-process-id - #'sb-posix:getpid)) diff --git a/src/infrastructure/acl/syscall-foreign.lisp b/src/infrastructure/acl/syscall-foreign.lisp index 64a7e37..13dea03 100644 --- a/src/infrastructure/acl/syscall-foreign.lisp +++ b/src/infrastructure/acl/syscall-foreign.lisp @@ -12,6 +12,11 @@ (request sb-alien:unsigned-long) (arg sb-sys:system-area-pointer)) +(defconstant +tiocgwinsz+ + #+darwin #x40087468 + #+linux #x5413 + #-(or darwin linux) 0) + (defconstant +tiocswinsz+ #+darwin #x80087467 #+linux #x5414 diff --git a/src/infrastructure/acl/syscall-process.lisp b/src/infrastructure/acl/syscall-process.lisp index 76aed2f..18da492 100644 --- a/src/infrastructure/acl/syscall-process.lisp +++ b/src/infrastructure/acl/syscall-process.lisp @@ -4,12 +4,6 @@ (defparameter *external-command-timeout* 30 "Maximum seconds for synchronous external commands. NIL disables the timeout.") -(defun %process-operation (function &rest arguments) - (handler-case - (values (apply function arguments) nil) - (error (condition) - (values nil condition)))) - (defun process-exit-status-code (proc) "Return shell-compatible exit status for an SBCL process." (let ((code (sb-ext:process-exit-code proc))) @@ -32,24 +26,26 @@ exit code, or 128+signal when it was terminated by a signal." while (plusp count) do (write-string buffer output :end count)) (when (streamp output) - (%process-operation #'finish-output output)))) + (ignore-errors + (finish-output output))))) -(defun start-stream-copier (input output name) - "Copy INPUT to OUTPUT asynchronously and return the copier thread." +(defun %start-stream-copier (input output name) (when input (sb-thread:make-thread (lambda () - (%process-operation #'%copy-process-output input output)) + (ignore-errors + (%copy-process-output input output))) :name name))) (defun %start-process-output-copier (proc output) - (start-stream-copier (and proc (sb-ext:process-output proc)) - output - "nshell process output copier")) + (%start-stream-copier (and proc (sb-ext:process-output proc)) + output + "nshell process output copier")) (defun %join-stream-copier (thread) (when thread - (%process-operation #'sb-thread:join-thread thread))) + (ignore-errors + (sb-thread:join-thread thread)))) (defun %join-process-output-copiers (copiers) (dolist (copier copiers) @@ -66,7 +62,7 @@ exit code, or 128+signal when it was terminated by a signal." do (sleep (min sleep-seconds (max 0 remaining-seconds))) (setf sleep-seconds (min 0.01 (* 2 sleep-seconds)))) (unless (sb-ext:process-alive-p proc) - (%process-operation #'sb-ext:process-wait proc) + (ignore-errors (sb-ext:process-wait proc)) t))) (defun %terminate-process (proc) @@ -77,23 +73,21 @@ exit code, or 128+signal when it was terminated by a signal." ;; negative PID can target nshells own group and terminate its host. (actual-pgid (and (integerp pid) (plusp pid) - (nth-value 0 (%process-operation #'sb-posix:getpgid pid)))) + (ignore-errors (sb-posix:getpgid pid)))) (owns-process-group-p (and (integerp actual-pgid) (plusp actual-pgid) (= pid actual-pgid)))) (flet ((terminate (signal) (if owns-process-group-p - (%process-operation #'%send-process-group-signal pid signal) + (ignore-errors (%send-process-group-signal pid signal)) (when (sb-ext:process-alive-p proc) - (%process-operation #'sb-ext:process-kill proc signal))))) + (ignore-errors (sb-ext:process-kill proc signal)))))) (terminate sb-unix:sigterm) (%wait-process-exit-with-timeout proc 0.5) (terminate sb-unix:sigkill) - (%process-operation #'sb-ext:process-wait proc))))) + (ignore-errors (sb-ext:process-wait proc)))))) -(defun wait-process-with-copiers - (proc copiers timeout-seconds success-fn timeout-fn) - "Wait for PROC while joining COPIERS, calling one of the result callbacks." +(defun %wait-process-with-copiers (proc copiers timeout-seconds success-fn timeout-fn) (unwind-protect (if (or (null timeout-seconds) (%wait-process-exit-with-timeout proc timeout-seconds)) @@ -109,7 +103,7 @@ exit code, or 128+signal when it was terminated by a signal." (defun %wait-process-with-output (proc output timeout-seconds timeout-fn) (let ((copier (%start-process-output-copier proc output))) - (wait-process-with-copiers + (%wait-process-with-copiers proc (list copier) timeout-seconds @@ -133,11 +127,9 @@ exit code, or 128+signal when it was terminated by a signal." return (subseq entry (length prefix))))) (defun %executable-file-p (path) - (handler-case - (not (zerop (logand (sb-posix:stat-mode (sb-posix:stat path)) - #o111))) - (error (condition) - (values nil condition)))) + (ignore-errors + (not (zerop (logand (sb-posix:stat-mode (sb-posix:stat path)) + #o111))))) (defun %resolve-external-command (command &optional (environment (%get-environment))) (nshell.domain.completion::%first-command-path-candidate @@ -156,19 +148,6 @@ exit code, or 128+signal when it was terminated by a signal." (defun %spawn-external-command (resolved-cmd args environment &key input output (error nil error-supplied-p)) (sb-ext:run-program resolved-cmd args :input input :output output :error (if error-supplied-p error (if *redirected-stderr* *error-output* :output)) :wait nil :search nil :environment environment)) -(defun %open-and-register-process-stream (pathname register &rest open-options) - (let ((stream nil) - (registered-p nil)) - (unwind-protect - (progn - (setf stream (apply #'open pathname open-options)) - (funcall register stream) - (setf registered-p t) - stream) - (unless registered-p - (when stream - (%process-operation #'close stream)))))) - (defun %resolve-input-redirect (redirects register) "Return the standard-input stream REDIRECTS ask for, calling REGISTER on any stream opened here so the caller can close it afterwards. With no input @@ -177,8 +156,7 @@ redirection the process inherits *STANDARD-INPUT*." (nshell.domain.parsing:redirect-input-spec redirects) (flet ((track (stream) (funcall register stream) stream)) (case kind - (:< (%open-and-register-process-stream - target register :direction :input :if-does-not-exist :error)) + (:< (track (open target :direction :input :if-does-not-exist :error))) (:<<< (track (%here-string-stream target))) ((:<< :<<-) (track (%here-document-stream target))) (t *standard-input*))))) @@ -189,9 +167,10 @@ process's own stdout, calling REGISTER on any stream opened here." (multiple-value-bind (target mode) (nshell.domain.parsing:redirect-output-spec redirects) (if target - (%open-and-register-process-stream - target register - :direction :output :if-exists mode :if-does-not-exist :create) + (let ((stream (open target :direction :output + :if-exists mode :if-does-not-exist :create))) + (funcall register stream) + stream) t))) (defun %spawn-in-own-process-group (resolved-cmd args environment input output &key (error nil error-supplied-p)) @@ -204,7 +183,7 @@ process's own stdout, calling REGISTER on any stream opened here." (when proc (let ((pid (sb-ext:process-pid proc))) (when (plusp pid) - (%process-operation #'set-process-group pid pid))) + (ignore-errors (set-process-group pid pid)))) proc))) (defun spawn-async (cmd args &key redirects) @@ -224,7 +203,7 @@ process's own stdout, calling REGISTER on any stream opened here." (format *error-output* "nshell: ~a: ~a~%" cmd err) nil)) (dolist (stream redirect-streams) - (%process-operation #'close stream)))))) + (ignore-errors (close stream))))))) (defun %foreground-external-command-timeout () "Return the timeout to apply to a foreground external command's wait: NIL @@ -276,7 +255,7 @@ normal foreground timeout policy." (format *error-output* "exec: ~a: ~a~%" cmd err) 1))) -(defun run-external (cmd args) + (defun run-external (cmd args) "Execute CMD with ARGS synchronously, printing output. Returns exit code." (handler-case (multiple-value-bind (resolved-cmd environment) @@ -285,8 +264,8 @@ normal foreground timeout policy." (%report-external-command-not-found cmd) (return-from run-external 127)) (let ((proc (%spawn-external-command resolved-cmd args environment - :input *standard-input* - :output :stream))) + :input *standard-input* + :output :stream))) (if proc (let* ((pid (sb-ext:process-pid proc)) (pgid (and (integerp pid) (plusp pid) pid)) diff --git a/src/infrastructure/acl/syscall-redirection.lisp b/src/infrastructure/acl/syscall-redirection.lisp index f45354f..4a4cab9 100644 --- a/src/infrastructure/acl/syscall-redirection.lisp +++ b/src/infrastructure/acl/syscall-redirection.lisp @@ -28,23 +28,7 @@ (defun %close-owned-redirect-stream (stream) (when (and stream (streamp stream)) - (handler-case - (values (close stream) nil) - (error (condition) - (values nil condition))))) - -(defun %install-redirect-stream (filename installer &rest open-options) - (let ((stream nil) - (installed-p nil)) - (unwind-protect - (progn - (setf stream (apply #'open filename open-options)) - (funcall installer stream) - (setf installed-p t) - stream) - (unless installed-p - (when stream - (%close-owned-redirect-stream stream)))))) + (ignore-errors (close stream)))) (defun %release-owned-stdout () (let ((stream *redirected-stdout-owned*)) @@ -52,47 +36,51 @@ (%close-owned-redirect-stream stream)) (setf *redirected-stdout-owned* nil))) +(defun %release-owned-stderr () + (let ((stream *redirected-stderr-owned*)) + (when (and stream (not (eq stream *redirected-stdout-owned*))) + (%close-owned-redirect-stream stream)) + (setf *redirected-stderr-owned* nil))) + +(defun %release-owned-stdin () + (%close-owned-redirect-stream *redirected-stdin-owned*) + (setf *redirected-stdin-owned* nil)) + (defun redirect-output (filename mode) "Redirect *STANDARD-OUTPUT* to FILENAME until RESTORE-REDIRECTS runs. MODE is a CL :IF-EXISTS value -- :SUPERSEDE for shell `>`, :APPEND for `>>`." - (%install-redirect-stream - filename - (lambda (stream) - (%remember-redirected-stdout) - (setf *redirected-stdout-owned* stream - *standard-output* stream)) - :direction :output - :if-exists mode - :if-does-not-exist :create)) + (let ((stream (open filename + :direction :output + :if-exists mode + :if-does-not-exist :create))) + (%remember-redirected-stdout) + (setf *redirected-stdout-owned* stream + *standard-output* stream))) (defun redirect-error (filename mode) "Redirect *ERROR-OUTPUT* to FILENAME until RESTORE-REDIRECTS runs. MODE is a CL :IF-EXISTS value -- :SUPERSEDE for shell `2>`, :APPEND for `2>>`." - (%install-redirect-stream - filename - (lambda (stream) - (%remember-redirected-stderr) - (setf *redirected-stderr-owned* stream - *error-output* stream)) - :direction :output - :if-exists mode - :if-does-not-exist :create)) + (let ((stream (open filename + :direction :output + :if-exists mode + :if-does-not-exist :create))) + (%remember-redirected-stderr) + (setf *redirected-stderr-owned* stream + *error-output* stream))) (defun redirect-output-and-error (filename mode) "Redirect both *STANDARD-OUTPUT* and *ERROR-OUTPUT* to the same open stream on FILENAME, for shell `&>`/`&>>`. MODE is a CL :IF-EXISTS value." - (%install-redirect-stream - filename - (lambda (stream) - (%remember-redirected-stdout) - (%remember-redirected-stderr) - (setf *redirected-stdout-owned* stream - *redirected-stderr-owned* stream - *standard-output* stream - *error-output* stream)) - :direction :output - :if-exists mode - :if-does-not-exist :create)) + (let ((stream (open filename + :direction :output + :if-exists mode + :if-does-not-exist :create))) + (%remember-redirected-stdout) + (%remember-redirected-stderr) + (setf *redirected-stdout-owned* stream + *redirected-stderr-owned* stream + *standard-output* stream + *error-output* stream))) (defun redirect-output-to-error () (%remember-redirected-stdout) @@ -112,14 +100,10 @@ keeps it from double-closing a stream this function never owned." (defun redirect-input (filename) "Redirect *STANDARD-INPUT* to read from FILENAME until RESTORE-REDIRECTS runs, for shell `<`. Signals if FILENAME does not exist." - (%install-redirect-stream - filename - (lambda (stream) - (%remember-redirected-stdin) - (setf *redirected-stdin-owned* stream - *standard-input* stream)) - :direction :input - :if-does-not-exist :error)) + (let ((stream (open filename :direction :input :if-does-not-exist :error))) + (%remember-redirected-stdin) + (setf *redirected-stdin-owned* stream + *standard-input* stream))) (defun redirect-input-string (value) "Redirect *STANDARD-INPUT* to VALUE plus a trailing newline, for a shell diff --git a/src/package-domain.lisp b/src/package-domain.lisp index fc9faa6..585fe8c 100644 --- a/src/package-domain.lisp +++ b/src/package-domain.lisp @@ -104,8 +104,7 @@ control-flow forms, reporting incomplete input so the reader can ask for a continuation line. A pure string-to-AST function with no filesystem access.") (:use #:cl) (:import-from #:nshell.util #:define-value-struct) - (:export #:tokenize #:balanced-substitution-end - #:shell-assignment-word-p #:parse-command-line + (:export #:tokenize #:shell-assignment-word-p #:parse-command-line #:shell-input-blank-p #:shell-word-separator-p #:shell-operator-separator-p #:shell-token-separator-p #:shell-command-separator-token-p @@ -215,7 +214,6 @@ that is what keeps expansion testable without a disk.") #:expand-variables #:expand-tilde #:expand-glob #:expand-all #:expand-by-quote-style #:expand-command-name-fields-by-quote-style - #:single-command-name-or-error #:expand-command-name-by-quote-style #:expand-double-quoted #:expand-arithmetic #:evaluate-arithmetic #:expand-braces #:argv-reference-fields #:*positional-args* diff --git a/src/package-highlight.lisp b/src/package-highlight.lisp deleted file mode 100644 index 400d5b4..0000000 --- a/src/package-highlight.lisp +++ /dev/null @@ -1,12 +0,0 @@ -;;; Shared syntax-highlighting package. - -(eval-when (:compile-toplevel :load-toplevel :execute) - (defpackage #:nshell.highlight - (:documentation - "Syntax-highlight data and rendering shared by application and presentation.") - (:use #:cl) - (:import-from #:nshell.util #:define-value-struct) - (:export #:highlight-line - #:highlight-span-start #:highlight-span-end - #:highlight-span-role - #:highlight->ansi #:theme-color->ansi))) diff --git a/src/package-infrastructure.lisp b/src/package-infrastructure.lisp index 7cb2310..8411c60 100644 --- a/src/package-infrastructure.lisp +++ b/src/package-infrastructure.lisp @@ -12,7 +12,6 @@ stores in the shell context, which is what makes the layers above it mockable.") (:use #:cl) (:import-from #:nshell.util #:define-value-struct) (:export #:*exported-environment* - #:make-process-fns #:spawn-pipeline #:spawn-pipeline-async #:wait-job #:spawn-process-substitution #:process-substitution-resource-p @@ -37,9 +36,8 @@ stores in the shell context, which is what makes the layers above it mockable.") #:child-status #:child-status-p #:child-status-pid #:child-status-status #:reap-children #:get-terminal-size #:terminal-size-unavailable #:terminal-size-unavailable-fd - #:*external-command-timeout* - #:start-stream-copier #:wait-process-with-copiers - #:run-external #:run-external-capture #:run-external-exec + #:*external-command-timeout* + #:run-external #:run-external-capture #:run-external-exec #:process-exit-status-code #:with-git-runner #:clear-git-status-cache #:get-git-status)) diff --git a/src/package-presentation.lisp b/src/package-presentation.lisp index 6026b23..c6c3667 100644 --- a/src/package-presentation.lisp +++ b/src/package-presentation.lisp @@ -7,8 +7,8 @@ "Presentation: everything the user sees and types at. Holds the line editor's input state and the reducer that advances it one key event at a time -- emacs and vi bindings, kill ring, undo, incremental history search, completion cycling --- plus autosuggestion, prompt drawing, and the REPL itself in its interactive, -batch, and script forms.") +-- plus syntax highlighting, autosuggestion, prompt drawing, and the REPL itself +in its interactive, batch, and script forms.") (:use #:cl) (:import-from #:nshell.util #:define-value-struct) (:export #:input-state #:input-state-p #:make-input-state @@ -36,6 +36,9 @@ batch, and script forms.") #:run-repl #:run-repl-batch #:run-repl-script #:trampoline #:render-prompt #:compute-suggestion #:accept-suggestion - #:render-completions #:apply-completion - #:segment-kind->role)) + #:render-completions #:apply-completion + #:highlight-line + #:highlight-span-start #:highlight-span-end + #:highlight-span-role + #:highlight->ansi #:theme-color->ansi #:segment-kind->role)) ) diff --git a/src/highlight.lisp b/src/presentation/highlight.lisp similarity index 80% rename from src/highlight.lisp rename to src/presentation/highlight.lisp index 05f2e9d..5edd6fc 100644 --- a/src/highlight.lisp +++ b/src/presentation/highlight.lisp @@ -1,15 +1,23 @@ -(in-package #:nshell.highlight) +(in-package #:nshell.presentation) +;; Highlight span value object; literal data tables live under data/. (define-value-struct %highlight-span ((start 0 :type integer) (end 0 :type integer) (role :normal :type keyword))) +;; -- Highlight roles (fish-inspired) ---------------------- (defun builtin-command-p (name) - (find name +highlight-builtin-names+ :test #'string=)) + (find name *builtin-commands* :test #'string=)) (defun classify-token-role (token-type token-value is-first-word) - "Map a token to its syntax-highlight role." + "Map a token to its highlight role. Follows fish shell conventions: + - first word: command (blue) or builtin (bright blue) + - subsequent words: argument (normal/cyan for options) + - pipes/redirects: operator (yellow) + - strings/quoted: quote (orange) + - errors: error (red) + - comments: comment (gray)" (case token-type (:word (cond @@ -56,12 +64,14 @@ role))) tokens))) +;; Rendering helpers for highlight spans. (defun fallback-highlight-control (role) (or (cdr (assoc role +fallback-highlight-ansi+ :test #'eq)) "~C[0m")) (defun theme-color->ansi (theme role) - "Convert a highlight ROLE to ANSI escape using THEME colors." + "Convert a highlight ROLE to ANSI escape using THEME colors. + Falls back to known ANSI 16-color codes when theme lookup fails." (let ((color (nshell.domain.configuration:theme-color theme role))) (if color (let ((code (nshell.infrastructure.terminal:ansi-color-code color))) @@ -73,13 +83,16 @@ (let ((result (make-string-output-stream)) (pos 0)) (dolist (span spans) + ;; Output unhighlighted gap (when (> (highlight-span-start span) pos) (write-string (subseq input pos (highlight-span-start span)) result)) + ;; Output highlighted span with color (format result "~a" (theme-color->ansi theme (highlight-span-role span))) (write-string (subseq input (highlight-span-start span) (highlight-span-end span)) result) (nshell.infrastructure.terminal:ansi-reset-style result) (setf pos (highlight-span-end span))) + ;; Output remaining text (when (< pos (length input)) (write-string (subseq input pos) result)) (get-output-stream-string result))) diff --git a/src/presentation/prompt-display.lisp b/src/presentation/prompt-display.lisp index c2a488b..3f32f5b 100644 --- a/src/presentation/prompt-display.lisp +++ b/src/presentation/prompt-display.lisp @@ -1,9 +1,9 @@ (in-package #:nshell.presentation) -(defun %char-visible-width (character) - "Terminal column width of CHARACTER, delegating to cl-tty-kit's Unicode-aware +(defun %char-visible-width (char) + "Terminal column width of CHAR, delegating to cl-tty-kit's Unicode-aware classifier (control/combining -> 0, wide/emoji -> 2, otherwise 1)." - (cl-tty-kit:char-width character)) + (cl-tty-kit:char-width char)) (defun %string-visible-width (text) "Sum of the terminal column widths of the characters in TEXT." @@ -53,9 +53,9 @@ it and treats a negative WIDTH as 0." segment) else append (let ((truncated (%truncate-string-to-width text remaining))) - (when (plusp (length truncated)) - (setf remaining 0) - (list (nshell.domain.prompting:make-prompt-segment + (when (plusp (length truncated)) + (setf remaining 0) + (list (nshell.domain.prompting:make-prompt-segment truncated kind))))))) @@ -65,8 +65,7 @@ it and treats a negative WIDTH as 0." (multiple-value-bind (rows cols) (nshell.infrastructure.acl:get-terminal-size) (declare (ignore rows)) (if (plusp cols) cols 80)) - (error (condition) - (values 80 condition)))) + (error () 80))) (defun segment-kind->role (kind) "Map prompt segment kind to highlight role for theme lookup." @@ -96,7 +95,7 @@ it and treats a negative WIDTH as 0." (let ((text (nshell.domain.prompting:prompt-segment-text seg)) (kind (nshell.domain.prompting:prompt-segment-kind seg))) (format stream "~a~a" - (nshell.highlight:theme-color->ansi theme (segment-kind->role kind)) + (theme-color->ansi theme (segment-kind->role kind)) text) (nshell.infrastructure.terminal:ansi-reset-style stream)))) diff --git a/src/presentation/repl-execution-context.lisp b/src/presentation/repl-execution-context.lisp index 290fac8..e916a5e 100644 --- a/src/presentation/repl-execution-context.lisp +++ b/src/presentation/repl-execution-context.lisp @@ -13,7 +13,12 @@ (not (null (host-kit:directory-exists-p path)))))) (defparameter +repl-process-fns+ - (nshell.infrastructure.acl:make-process-fns)) + (list :run-external + (lambda (command args) + (nshell.infrastructure.acl:run-external command args)) + :run-external-capture + (lambda (command args) + (nshell.infrastructure.acl:run-external-capture command args)))) (defparameter +repl-redirect-fns+ (list :redirect-output #'nshell.infrastructure.acl:redirect-output diff --git a/src/presentation/repl-output-completion-help.lisp b/src/presentation/repl-output-completion-help.lisp index c43d839..5b51a36 100644 --- a/src/presentation/repl-output-completion-help.lisp +++ b/src/presentation/repl-output-completion-help.lisp @@ -59,6 +59,9 @@ (setf (gethash (first spec) cache) t)))) "Set of catalogued external commands that should be enriched from help output.") +(defun %completion-help-catalogued-command-p (command) + (gethash command *completion-help-catalog-command-cache*)) + (defun %completion-help-text-p (output) (and (stringp output) (plusp (length output)) @@ -94,8 +97,8 @@ (multiple-value-bind (output exit-code) (funcall *completion-help-fetcher* command) (%completion-help-cache-help-text command output exit-code)) - (error (condition) - (values :missing condition)))) + (error () + :missing))) (progn (defun %warm-command-completion-help (command) diff --git a/src/presentation/repl-rendering.lisp b/src/presentation/repl-rendering.lisp index 7625799..32ce750 100644 --- a/src/presentation/repl-rendering.lisp +++ b/src/presentation/repl-rendering.lisp @@ -33,12 +33,9 @@ (format t "~C[7m~a~C[27m" #\Esc segment #\Esc) (handler-case (format t "~a" - (nshell.highlight:highlight->ansi - (nshell.highlight:highlight-line segment) - segment theme)) - (error (condition) - (format t "~a" segment) - (values nil condition)))) + (highlight->ansi (highlight-line segment) segment theme)) + (error () + (format t "~a" segment)))) (setf local-start local-end)))) (defun render-edit-buffer (text theme &key selection-start selection-end) @@ -84,8 +81,7 @@ (setf *prompt-rendered-origin-row* row *prompt-rendered-origin-column* column)) (setf *prompt-rendered-origin-known-p* t)) - (error (condition) - (values nil condition)))) + (error () nil))) (unless *prompt-rendered-origin-known-p* (setf *prompt-rendered-origin-row* 1 *prompt-rendered-origin-column* 1 @@ -118,7 +114,7 @@ (let* ((terminal-width (multiple-value-bind (rows cols) (handler-case (nshell.infrastructure.acl:get-terminal-size) - (error (condition) (values 24 80 condition))) + (error () (values 24 80))) (declare (ignore rows)) cols)) (prompt-width @@ -164,5 +160,5 @@ *prompt-rendered-cursor-row* (rendered-position-row cursor-position) *prompt-rendered-terminal-width* terminal-width *prompt-rendered-prompt-width* prompt-width)) - (finish-output) - (lambda () (read-key-cont)))) + (finish-output) + (lambda () (read-key-cont)))) diff --git a/t/integration/test-file-config.lisp b/t/integration/test-file-config.lisp index 5846fd2..4aee6e1 100644 --- a/t/integration/test-file-config.lisp +++ b/t/integration/test-file-config.lisp @@ -3,62 +3,63 @@ (describe "file-config-tests" (it "file-config-missing-file" "Loading a missing config file returns NIL." - (host-kit:with-temporary-directory (directory) - (let ((test-path (merge-pathnames "config-missing.lisp" directory))) - (unwind-protect - (progn - (setf nshell.infrastructure.persistence::*config-file-path-override* - test-path) - (when (probe-file test-path) - (delete-file test-path)) - (expect (nshell.infrastructure.persistence:load-config) :to-be-null)) - (setf nshell.infrastructure.persistence::*config-file-path-override* nil) - (when (probe-file test-path) - (delete-file test-path)))))) + (let* ((test-path (format nil "/tmp/nshell-test-config-missing-~d.lisp" + (random 1000000)))) + (unwind-protect + (progn + (setf nshell.infrastructure.persistence::*config-file-path-override* + (pathname test-path)) + (when (probe-file test-path) + (delete-file test-path)) + (expect (nshell.infrastructure.persistence:load-config) :to-be-null)) + (setf nshell.infrastructure.persistence::*config-file-path-override* nil) + (when (probe-file test-path) + (delete-file test-path))))) (it "file-config-roundtrip" "Saving and loading config preserves the stored lines." - (host-kit:with-temporary-directory (directory) - (let ((test-path (merge-pathnames "config.lisp" directory)) - (config '("set -g prompt nshell" - "export PATH=/usr/local/bin:$PATH" - ""))) - (unwind-protect - (progn - (setf nshell.infrastructure.persistence::*config-file-path-override* - test-path) - (when (probe-file test-path) - (delete-file test-path)) - (expect t :to-be (nshell.infrastructure.persistence:save-config config)) - (expect config :to-equal (nshell.infrastructure.persistence:load-config))) - (setf nshell.infrastructure.persistence::*config-file-path-override* nil) - (when (probe-file test-path) - (delete-file test-path)))))) + (let* ((test-path (format nil "/tmp/nshell-test-config-~d.lisp" + (random 1000000))) + (config '("set -g prompt nshell" + "export PATH=/usr/local/bin:$PATH" + ""))) + (unwind-protect + (progn + (setf nshell.infrastructure.persistence::*config-file-path-override* + (pathname test-path)) + (when (probe-file test-path) + (delete-file test-path)) + (expect t :to-be (nshell.infrastructure.persistence:save-config config)) + (expect config :to-equal (nshell.infrastructure.persistence:load-config))) + (setf nshell.infrastructure.persistence::*config-file-path-override* nil) + (when (probe-file test-path) + (delete-file test-path))))) (it "interactive-startup-sources-config" "Interactive initialization applies .nshellrc through the normal source boundary." - (host-kit:with-temporary-directory (directory) - (let ((config-path (merge-pathnames "startup-config.lisp" directory)) - (history-path (merge-pathnames "startup-history" directory))) - (unwind-protect - (progn - (with-open-file (stream config-path - :direction :output - :if-exists :supersede - :if-does-not-exist :create) - (write-line "set -x NSHELL_CONFIG_LOADED yes" stream)) - (setf nshell.infrastructure.persistence::*config-file-path-override* - config-path - nshell.infrastructure.persistence::*history-file-path-override* - history-path) - (nshell.presentation::initialize-repl-state) - (expect "yes" :to-equal (repl-test-env "NSHELL_CONFIG_LOADED"))) - (setf nshell.infrastructure.persistence::*config-file-path-override* nil - nshell.infrastructure.persistence::*history-file-path-override* nil) - (when (probe-file config-path) - (delete-file config-path)) - (when (probe-file history-path) - (delete-file history-path)))))) + (let* ((config-path (format nil "/tmp/nshell-test-startup-config-~d.lisp" + (random 1000000))) + (history-path (format nil "/tmp/nshell-test-startup-history-~d" + (random 1000000)))) + (unwind-protect + (progn + (with-open-file (stream config-path + :direction :output + :if-exists :supersede + :if-does-not-exist :create) + (write-line "set -x NSHELL_CONFIG_LOADED yes" stream)) + (setf nshell.infrastructure.persistence::*config-file-path-override* + (pathname config-path) + nshell.infrastructure.persistence::*history-file-path-override* + (pathname history-path)) + (nshell.presentation::initialize-repl-state) + (expect "yes" :to-equal (repl-test-env "NSHELL_CONFIG_LOADED"))) + (setf nshell.infrastructure.persistence::*config-file-path-override* nil + nshell.infrastructure.persistence::*history-file-path-override* nil) + (when (probe-file config-path) + (delete-file config-path)) + (when (probe-file history-path) + (delete-file history-path))))) (it "interactive-config-reports-nonzero-status" "A nonzero sourced config status is reported on standard error." (with-temporary-functions diff --git a/t/integration/test-file-history.lisp b/t/integration/test-file-history.lisp index e2dd88d..8aa4464 100644 --- a/t/integration/test-file-history.lisp +++ b/t/integration/test-file-history.lisp @@ -3,65 +3,64 @@ (describe "file-history-tests" (it "file-history-append" "Appending to file history works" - (host-kit:with-temporary-directory (directory) - (let ((test-path (merge-pathnames "history.lisp" directory))) - (unwind-protect - (progn - ;; Override history file path for test isolation - (setf nshell.infrastructure.persistence:*history-file-path-override* - test-path) - ;; Clean up any previous test data - (when (probe-file test-path) (delete-file test-path)) - (nshell.infrastructure.persistence:append-history-entry "test command") - (let ((loaded (nshell.infrastructure.persistence:load-history-file))) - (expect (consp loaded) :to-be-truthy) - (expect "test command" :to-equal (first loaded)))) - ;; Cleanup - (setf nshell.infrastructure.persistence:*history-file-path-override* nil) - (when (probe-file test-path) (delete-file test-path)))))) + (let* ((test-path (format nil "/tmp/nshell-test-history-~d.lisp" (random 1000000)))) + (unwind-protect + (progn + ;; Override history file path for test isolation + (setf nshell.infrastructure.persistence:*history-file-path-override* + (pathname test-path)) + ;; Clean up any previous test data + (when (probe-file test-path) (delete-file test-path)) + (nshell.infrastructure.persistence:append-history-entry "test command") + (let ((loaded (nshell.infrastructure.persistence:load-history-file))) + (expect (consp loaded) :to-be-truthy) + (expect "test command" :to-equal (first loaded)))) + ;; Cleanup + (setf nshell.infrastructure.persistence:*history-file-path-override* nil) + (when (probe-file test-path) (delete-file test-path))))) (it "file-history-multiline-round-trip" "Multiline command history survives persistence." - (host-kit:with-temporary-directory (directory) - (let ((test-path (merge-pathnames "history-multiline.lisp" directory)) - (command (format nil "for item in one two~% echo $item~%done"))) - (unwind-protect - (progn - (setf nshell.infrastructure.persistence:*history-file-path-override* - test-path) - (when (probe-file test-path) (delete-file test-path)) - (nshell.infrastructure.persistence:append-history-entry command) - (expect (list command) :to-equal - (nshell.infrastructure.persistence:load-history-file))) - (setf nshell.infrastructure.persistence:*history-file-path-override* nil) - (when (probe-file test-path) (delete-file test-path)))))) + (let* ((test-path (format nil "/tmp/nshell-test-history-multiline-~d.lisp" + (random 1000000))) + (command (format nil "for item in one two~% echo $item~%done"))) + (unwind-protect + (progn + (setf nshell.infrastructure.persistence:*history-file-path-override* + (pathname test-path)) + (when (probe-file test-path) (delete-file test-path)) + (nshell.infrastructure.persistence:append-history-entry command) + (expect (list command) :to-equal + (nshell.infrastructure.persistence:load-history-file))) + (setf nshell.infrastructure.persistence:*history-file-path-override* nil) + (when (probe-file test-path) (delete-file test-path))))) (it "file-history-unframed-lines" "Unframed history records are not loaded." - (host-kit:with-temporary-directory (directory) - (let ((test-path (merge-pathnames "history-unframed.lisp" directory))) - (unwind-protect - (progn - (setf nshell.infrastructure.persistence:*history-file-path-override* - test-path) - (when (probe-file test-path) (delete-file test-path)) - (with-open-file (stream test-path :direction :output - :if-exists :supersede - :if-does-not-exist :create) - (format stream "unframed one~%unframed two~%")) - (expect (nshell.infrastructure.persistence:load-history-file) :to-be-null)) - (setf nshell.infrastructure.persistence:*history-file-path-override* nil) - (when (probe-file test-path) (delete-file test-path)))))) + (let ((test-path (format nil "/tmp/nshell-test-history-unframed-~d.lisp" + (random 1000000)))) + (unwind-protect + (progn + (setf nshell.infrastructure.persistence:*history-file-path-override* + (pathname test-path)) + (when (probe-file test-path) (delete-file test-path)) + (with-open-file (stream test-path :direction :output + :if-exists :supersede + :if-does-not-exist :create) + (format stream "unframed one~%unframed two~%")) + (expect (nshell.infrastructure.persistence:load-history-file) :to-be-null)) + (setf nshell.infrastructure.persistence:*history-file-path-override* nil) + (when (probe-file test-path) (delete-file test-path))))) (it "file-history-missing-file" "Loading a missing history file returns NIL." - (host-kit:with-temporary-directory (directory) - (let ((test-path (merge-pathnames "history-missing.lisp" directory))) - (unwind-protect - (progn - (setf nshell.infrastructure.persistence:*history-file-path-override* - test-path) - (when (probe-file test-path) (delete-file test-path)) - (expect (nshell.infrastructure.persistence:load-history-file) :to-be-null)) - (setf nshell.infrastructure.persistence:*history-file-path-override* nil) - (when (probe-file test-path) (delete-file test-path))))))) + (let* ((test-path (format nil "/tmp/nshell-test-history-missing-~d.lisp" + (random 1000000)))) + (unwind-protect + (progn + (setf nshell.infrastructure.persistence:*history-file-path-override* + (pathname test-path)) + (when (probe-file test-path) (delete-file test-path)) + (expect (nshell.infrastructure.persistence:load-history-file) :to-be-null)) + (setf nshell.infrastructure.persistence:*history-file-path-override* nil) + (when (probe-file test-path) (delete-file test-path)))))) diff --git a/t/integration/test-terminal-presentation.lisp b/t/integration/test-terminal-presentation.lisp index fa8738f..d986f4f 100644 --- a/t/integration/test-terminal-presentation.lisp +++ b/t/integration/test-terminal-presentation.lisp @@ -6,9 +6,9 @@ (dolist (line (list "echo \"hi" "echo \\")) (let ((state (input-state))) (dolist (event (read-key-events-from-string (format nil "~a~%" line))) - (multiple-value-bind (next-state output) + (multiple-value-bind (next-state output) (nshell.presentation:reduce-input-state state event) - (setf state + (setf state (if (eq output :execute) (with-parsed-command-line (result (nshell.presentation:input-state-buffer next-state)) @@ -24,48 +24,50 @@ (it "terminal-execute-on-structural-incomplete-input-indents-continuation" "REPL execution promotes structural incomplete input to an indented continuation." (with-repl-test-state - (dolist (line '("echo hi |" "echo hi &&" "if true")) - (let ((expected (concatenate 'string line (string #\Newline) " "))) + (dolist (case '(("echo hi |" . "echo hi |~% ") + ("echo hi &&" . "echo hi &&~% ") + ("if true" . "if true~% "))) + (destructuring-bind (line . expected-format) case (setf nshell.presentation::*input-state* (nshell.presentation::make-repl-input-state :buffer line)) (capture-process-output-event :execute) - (expect expected :to-equal (nshell.presentation:input-state-buffer - nshell.presentation::*input-state*)) + (expect (format nil expected-format) :to-equal (nshell.presentation:input-state-buffer + nshell.presentation::*input-state*)) (expect (length (nshell.presentation:input-state-buffer nshell.presentation::*input-state*)) :to-equal (nshell.presentation:input-state-cursor-pos nshell.presentation::*input-state*)))))) (it "terminal-highlight-uses-parser-diagnostics" "Presentation highlighting marks parser diagnostics as errors." - (let* ((spans (nshell.highlight:highlight-line "| echo nope")) + (let* ((spans (nshell.presentation:highlight-line "| echo nope")) (first-span (first spans))) (expect (null first-span) :to-be-falsy) - (expect :error :to-be (nshell.highlight:highlight-span-role first-span)) - (expect 0 :to-equal (nshell.highlight:highlight-span-start first-span)) - (expect 1 :to-equal (nshell.highlight:highlight-span-end first-span)))) + (expect :error :to-be (nshell.presentation:highlight-span-role first-span)) + (expect 0 :to-equal (nshell.presentation:highlight-span-start first-span)) + (expect 1 :to-equal (nshell.presentation:highlight-span-end first-span)))) (it "terminal-highlight-span-constructor-is-internal-boundary" "Highlight spans are produced by highlight-line rather than public raw construction." - (expect (fboundp 'nshell.highlight::make-highlight-span) :to-be-falsy) - (expect (fboundp 'nshell.highlight::highlight-span-p) :to-be-falsy) - (expect (fboundp 'nshell.highlight::copy-highlight-span) :to-be-falsy) - (expect (fboundp 'nshell.highlight::%make-highlight-span) :to-be-truthy)) + (expect (fboundp 'nshell.presentation::make-highlight-span) :to-be-falsy) + (expect (fboundp 'nshell.presentation::highlight-span-p) :to-be-falsy) + (expect (fboundp 'nshell.presentation::copy-highlight-span) :to-be-falsy) + (expect (fboundp 'nshell.presentation::%make-highlight-span) :to-be-truthy)) (it "terminal-highlight-span-type-is-internal-boundary" "Highlight spans are opaque presentation values with public projections only." - (let ((span (first (nshell.highlight:highlight-line "| echo nope")))) - (expect span :to-be-type-of 'nshell.highlight::%highlight-span) - (expect (find-symbol "HIGHLIGHT-SPAN" :nshell.highlight) :to-be-falsy))) + (let ((span (first (nshell.presentation:highlight-line "| echo nope")))) + (expect span :to-be-type-of 'nshell.presentation::%highlight-span) + (expect (find-symbol "HIGHLIGHT-SPAN" :nshell.presentation) :to-be-falsy))) (it "terminal-highlight-span-raw-accessors-stay-internal" "Highlight span projections stay behind explicit public accessors." - (let ((span (first (nshell.highlight:highlight-line "| echo nope")))) - (expect (fboundp 'nshell.highlight::%highlight-span-start) :to-be-truthy) - (expect (fboundp 'nshell.highlight::%highlight-span-end) :to-be-truthy) - (expect (fboundp 'nshell.highlight::%highlight-span-role) :to-be-truthy) - (expect (nshell.highlight:highlight-span-start span) :to-equal (nshell.highlight::%highlight-span-start span)) - (expect (nshell.highlight:highlight-span-end span) :to-equal (nshell.highlight::%highlight-span-end span)) - (expect (nshell.highlight:highlight-span-role span) :to-be (nshell.highlight::%highlight-span-role span)))) + (let ((span (first (nshell.presentation:highlight-line "| echo nope")))) + (expect (fboundp 'nshell.presentation::%highlight-span-start) :to-be-truthy) + (expect (fboundp 'nshell.presentation::%highlight-span-end) :to-be-truthy) + (expect (fboundp 'nshell.presentation::%highlight-span-role) :to-be-truthy) + (expect (nshell.presentation:highlight-span-start span) :to-equal (nshell.presentation::%highlight-span-start span)) + (expect (nshell.presentation:highlight-span-end span) :to-equal (nshell.presentation::%highlight-span-end span)) + (expect (nshell.presentation:highlight-span-role span) :to-be (nshell.presentation::%highlight-span-role span)))) (it "terminal-highlight-uses-public-ansi-boundary" "Presentation color rendering depends on the terminal ANSI public contract." diff --git a/t/support/builtins.lisp b/t/support/builtins.lisp index e2c941b..9b62460 100644 --- a/t/support/builtins.lisp +++ b/t/support/builtins.lisp @@ -29,18 +29,14 @@ :cwd (lambda () #p"/tmp/") :chdir (lambda (path) (declare (ignore path)) t)) :process-fns - ;; :run-external and :run-external-capture are listed ahead of - ;; MAKE-PROCESS-FNS's real implementations so GETF's first-match finds - ;; these fakes instead: an absent capture-runner must stay absent (NIL) - ;; so %RUN-EXTERNAL-COMMAND-IN-CONTEXT falls back to :run-external, - ;; rather than silently picking up the real external-process runner. - (list* :run-external - (or external-runner - (lambda (command args) - (declare (ignore command args)) - 0)) - :run-external-capture external-capture-runner - (nshell.infrastructure.acl:make-process-fns)) + (let ((fns (list :run-external + (or external-runner + (lambda (command args) + (declare (ignore command args)) + 0))))) + (if external-capture-runner + (list* :run-external-capture external-capture-runner fns) + fns)) :redirect-fns (list :redirect-output #'nshell.infrastructure.acl:redirect-output :redirect-error #'nshell.infrastructure.acl:redirect-error @@ -98,13 +94,13 @@ output-empty contains) (append (when (not (null code)) - `((expect ,actual-code :to-equal ,code))) + `((expect ,code :to-equal ,actual-code))) (when output - `((expect ,actual-output :to-equal ,output))) + `((expect ,output :to-equal ,actual-output))) (when output-null `((expect ,actual-output :to-be-null))) (when output-empty - `((expect ,actual-output :to-equal ""))) + `((expect "" :to-equal ,actual-output))) (when contains `((expect (%builtin-output-contains-all-p ,actual-output ,contains) :to-be-truthy))) `((values ,actual-output ,actual-code)))) @@ -143,8 +139,8 @@ (defmacro with-builtins-source-ok ((output code context lines) expected-output &body extra-assertions) "Like WITH-BUILTINS-SOURCE but automatically asserts exit-code=0 and string output equality." `(with-builtins-source (,output ,code ,context ,lines) - (expect ,code :to-equal 0) - (expect ,output :to-equal ,expected-output) + (expect 0 :to-equal ,code) + (expect ,expected-output :to-equal ,output) ,@extra-assertions)) (defmacro with-stubbed-command-executor ((&rest cases) &body body) @@ -197,23 +193,23 @@ form should return (values output exit-code). Example: (multiple-value-bind (output code) (call-builtin ,context ,name ,add-args) (expect output :to-be-null) - (expect code :to-equal 0)) - (expect (gethash ,key ,table-form) :to-equal ,expansion) - (expect (nth-value 1 (call-builtin ,context ,name (list "-q" ,key))) :to-equal 0) - (expect (nth-value 1 (call-builtin ,context ,name (list "-q" ,missing-key))) :to-equal 1) + (expect 0 :to-equal code)) + (expect ,expansion :to-equal (gethash ,key ,table-form)) + (expect 0 :to-equal (nth-value 1 (call-builtin ,context ,name (list "-q" ,key)))) + (expect 1 :to-equal (nth-value 1 (call-builtin ,context ,name (list "-q" ,missing-key)))) ,@(when body-contains `((multiple-value-bind (output code) (call-builtin ,context ,name (list ,key)) - (expect code :to-equal 0) + (expect 0 :to-equal code) ,@(mapcar (lambda (needle) `(expect (search ,needle output) :to-be-truthy)) body-contains)))) (multiple-value-bind (output code) (call-builtin ,context ,name nil) - (expect code :to-equal 0) + (expect 0 :to-equal code) (expect (search ,list-fragment output) :to-be-truthy)) (assert-builtin-call (,context ,name '("-e")) :code 2 :output ,erase-error-output) - (expect (nth-value 1 (call-builtin ,context ,name ,erase-args)) :to-equal 0) + (expect 0 :to-equal (nth-value 1 (call-builtin ,context ,name ,erase-args))) (expect (gethash ,key ,table-form) :to-be-null))) diff --git a/t/support/pbt-shell.lisp b/t/support/pbt-shell.lisp index 21d9193..61434d7 100644 --- a/t/support/pbt-shell.lisp +++ b/t/support/pbt-shell.lisp @@ -20,23 +20,21 @@ t))) (defun %default-test-process-fns () - (append - (list :spawn (lambda (&rest args) - (declare (ignore args)) - :spawned) - :wait (lambda (&rest args) + (list :spawn (lambda (&rest args) (declare (ignore args)) - :waited) - :signal (lambda (&rest args) - (declare (ignore args)) - :signaled) - :run-external (lambda (command args) - (declare (ignore command args)) - 0) - :run-external-capture (lambda (command args) - (declare (ignore command args)) - (values nil 0))) - (nshell.infrastructure.acl:make-process-fns))) + :spawned) + :wait (lambda (&rest args) + (declare (ignore args)) + :waited) + :signal (lambda (&rest args) + (declare (ignore args)) + :signaled) + :run-external (lambda (command args) + (declare (ignore command args)) + 0) + :run-external-capture (lambda (command args) + (declare (ignore command args)) + (values nil 0)))) (defun %default-test-terminal-fns () (list :get-size (lambda () diff --git a/t/unit/test-builtins-core-io.lisp b/t/unit/test-builtins-core-io.lisp index f5dccd6..a368719 100644 --- a/t/unit/test-builtins-core-io.lisp +++ b/t/unit/test-builtins-core-io.lisp @@ -30,113 +30,6 @@ :code 0 :output (format nil "literal~%")))) - (it "printf-covers-option-escape-and-formatting-boundaries" - "printf handles its option terminator, escape forms, widths, precisions, and errors." - (with-builtins-context (context) - (assert-builtin-cases (context "printf") - (nil - :code 0 - :output-null t) - (("--" "%s" "x") - :code 0 - :output "x") - (("%b" "left\\cignored") - :code 0 - :output "left") - (("%b" "A\\010B") - :code 0 - :output (concatenate 'string "A" (string #\Backspace) "B")) - (("%b" "\\a\\b\\e\\f\\n\\r\\t\\v\\\\") - :code 0 - :output (concatenate 'string - (string (code-char 7)) - (string #\Backspace) - (string (code-char 27)) - (string #\Page) - (string #\Newline) - (string #\Return) - (string #\Tab) - (string (code-char 11)) - "\\")) - (("literal\\q") - :code 0 - :output "literalq") - (("trailing\\") - :code 0 - :output "trailing\\") - (("%%") - :code 0 - :output "%") - (("%c" "Z") - :code 0 - :output "Z") - (("%c" "") - :code 0 - :output (string #\Null)) - (("%+d" "7") - :code 0 - :output "+7") - (("% d" "7") - :code 0 - :output " 7") - (("%i" "-7") - :code 0 - :output "-7") - (("%u" "7") - :code 0 - :output "7") - (("%#o" "8") - :code 0 - :output "010") - (("%#x" "15") - :code 0 - :output "0xf") - (("%#X" "15") - :code 0 - :output "0XF") - (("%.3d" "7") - :code 0 - :output "007") - (("%05.3d" "7") - :code 0 - :output " 007") - (("%-5s" "x") - :code 0 - :output "x ") - (("%2lls" "x") - :code 0 - :output " x") - (("%.s" "x") - :code 0 - :output-empty t) - (("%f" "1.5") - :code 0 - :output (format nil "~,6f" 1.5d0)) - (("%.2f" "1.5") - :code 0 - :output (format nil "~,2f" 1.5d0)) - (("%+f" "1.5") - :code 0 - :output (concatenate 'string "+" (format nil "~,6f" 1.5d0))) - (("% f" "1.5") - :code 0 - :output (concatenate 'string " " (format nil "~,6f" 1.5d0))) - (("%E" "1.5") - :code 0 - :contains '("E")) - (("%f" "not-a-number") - :code 1 - :output (format nil "~,6f" 0d0)) - (("%q" "x") - :code 1 - :output-empty t) - (("%") - :code 1 - :output-empty t) - (("literal" "ignored") - :code 0 - :output "literal")))) - (it "pwd-returns-current-working-directory" "pwd returns the context cwd as a string with a trailing newline." (with-builtins-context (context) diff --git a/t/unit/test-builtins-source.lisp b/t/unit/test-builtins-source.lisp index 7e344ca..dde857e 100644 --- a/t/unit/test-builtins-source.lisp +++ b/t/unit/test-builtins-source.lisp @@ -130,8 +130,7 @@ process)) (with-temporary-function ('nshell.application::%background-process-pid - (lambda (ignored-context proc) - (declare (ignore ignored-context)) + (lambda (proc) (when (eq proc process) 4321))) (with-called-source (output code context '("first arg & second")) @@ -171,8 +170,7 @@ processes)) (with-temporary-function ('nshell.application::%background-process-pid - (lambda (ignored-context proc) - (declare (ignore ignored-context)) + (lambda (proc) (case proc (:left-process 4321) (:right-process 4322)))) diff --git a/t/unit/test-execute-pipeline.lisp b/t/unit/test-execute-pipeline.lisp index 034c3b0..2c27b35 100644 --- a/t/unit/test-execute-pipeline.lisp +++ b/t/unit/test-execute-pipeline.lisp @@ -54,9 +54,7 @@ :process-exited :pipeline-completed) (nshell.domain.events:domain-event-type event) - (expect 0 :to-equal - (nshell.application:execute-pipeline-use-case - ast dispatcher (%default-test-process-fns))) + (expect 0 :to-equal (nshell.application:execute-pipeline-use-case ast dispatcher)) (expect (nshell.application:drain-events dispatcher) :to-be-null) (let ((delivered (nreverse events))) (expect (member :pipeline-started delivered) :to-be-truthy) @@ -71,8 +69,7 @@ (code nil) (output (with-output-to-string (*standard-output*) (setf code - (nshell.application:execute-pipeline-use-case - ast nil (%default-test-process-fns)))))) + (nshell.application:execute-pipeline-use-case ast nil))))) (expect 0 :to-equal code) (expect "pipeline-api" :to-equal output)))) @@ -85,8 +82,7 @@ (code nil)) (let ((error-output (with-output-to-string (*error-output*) - (setf code (nshell.application:execute-pipeline-use-case - ast nil (%default-test-process-fns)))))) + (setf code (nshell.application:execute-pipeline-use-case ast nil))))) (expect 127 :to-equal code) (expect (format nil "nshell: $NSHELL_PIPELINE_CMD: command name expansion produced 2 fields~%") :to-equal error-output))))) @@ -98,8 +94,7 @@ (code nil)) (let ((error-output (with-output-to-string (*error-output*) - (setf code (nshell.application:execute-pipeline-use-case - ast nil (%default-test-process-fns)))))) + (setf code (nshell.application:execute-pipeline-use-case ast nil))))) (expect 127 :to-equal code) (expect (format nil "nshell: NSHELL_MISSING_REQUIRED: required value~%") :to-equal error-output)))) @@ -143,16 +138,16 @@ (unwind-protect (progn (ensure-directories-exist root) - (expect 0 :to-equal - (nshell.application:execute-pipeline-use-case - ast nil (%default-test-process-fns))) + (expect 0 :to-equal (nshell.application:execute-pipeline-use-case ast nil)) (expect (probe-file output) :to-be-truthy) (with-open-file (stream output :direction :input) (let ((actual (make-string (file-length stream)))) (read-sequence actual stream) (expect content :to-equal actual)))) - (when (probe-file root) - (host-kit:delete-directory-tree root :validate t))))) + (handler-case + (when (probe-file root) + (host-kit:delete-directory-tree root :validate t)) + (error ()))))) (it "pipeline-stage-streams-opens-file-and-here-string-inputs" "Pipeline stage setup materializes file and here-string input redirects." @@ -250,8 +245,7 @@ (ast (nshell.domain.parsing:make-pipeline-node (list writer counter))) (code nil) (output (capture-standard-output - (setf code (nshell.application:execute-pipeline-use-case - ast nil (%default-test-process-fns)))))) + (setf code (nshell.application:execute-pipeline-use-case ast nil))))) (expect 0 :to-equal code) (expect (format nil "3~%") :to-equal output))) @@ -265,8 +259,7 @@ (ast (nshell.domain.parsing:make-pipeline-node (list writer counter))) (code nil) (output (capture-standard-output - (setf code (nshell.application:execute-pipeline-use-case - ast nil (%default-test-process-fns)))))) + (setf code (nshell.application:execute-pipeline-use-case ast nil))))) (expect 0 :to-equal code) (expect (format nil "6~%") :to-equal output))) @@ -281,8 +274,7 @@ (ast (nshell.domain.parsing:make-pipeline-node (list writer counter))) (code nil) (output (capture-standard-output - (setf code (nshell.application:execute-pipeline-use-case - ast nil (%default-test-process-fns)))))) + (setf code (nshell.application:execute-pipeline-use-case ast nil))))) (expect 0 :to-equal code) (expect (format nil "3~%") :to-equal output) (expect "OUT" :to-equal (host-kit:read-file-string target))))) @@ -298,8 +290,7 @@ (ast (nshell.domain.parsing:make-pipeline-node (list writer counter))) (code nil) (output (capture-standard-output - (setf code (nshell.application:execute-pipeline-use-case - ast nil (%default-test-process-fns)))))) + (setf code (nshell.application:execute-pipeline-use-case ast nil))))) (expect 0 :to-equal code) (expect (format nil "0~%") :to-equal output) (expect "OUTERR" :to-equal (host-kit:read-file-string target))))) @@ -309,9 +300,7 @@ (let ((ast (nshell.domain.parsing:make-command-node "definitely-not-a-real-command" nil))) - (expect 127 :to-equal - (nshell.application:execute-pipeline-use-case - ast nil (%default-test-process-fns))))) + (expect 127 :to-equal (nshell.application:execute-pipeline-use-case ast nil)))) (it "execute-pipeline-node-in-context-times-out-external-stages-in-cps-mode" "The CPS execution path drains external output and times out long-running stages." @@ -444,8 +433,7 @@ (length (nshell.application::%process-substitution-inner-commands pipeline)))) (expect (nshell.application::%process-substitution-inner-commands nil) :to-be-null)) (it "process-substitution-resource-cleanup-is-best-effort" - (let ((context (make-test-shell-context)) - (released nil)) + (let ((released nil)) (with-temporary-functions (((quote nshell.infrastructure.acl:release-process-substitution-fd) (lambda (resource) @@ -453,12 +441,10 @@ (when (eq resource :bad) (error "release failure"))))) (nshell.application::%release-process-substitution-resources - context (list :first :bad :last))) (expect (list :last :bad :first) :to-equal released))) (it "process-substitution-finish-and-abort-release-all-resources" - (let ((context (make-test-shell-context)) - (waited nil) + (let ((waited nil) (released nil) (closed nil)) (with-temporary-functions @@ -473,8 +459,8 @@ ((quote nshell.infrastructure.acl:close-process-substitution) (lambda (resource) (push resource closed)))) - (nshell.application::%finish-process-substitution-resources context (list :ok :bad)) - (nshell.application::%abort-process-substitution-resources context (list :left :right))) + (nshell.application::%finish-process-substitution-resources (list :ok :bad)) + (nshell.application::%abort-process-substitution-resources (list :left :right))) (expect (list :bad :ok) :to-equal waited) (expect (list :bad :ok) :to-equal released) (expect (list :right :left) :to-equal closed))) @@ -486,7 +472,6 @@ (expect (list 3 4) :to-equal (nshell.application::%process-substitution-resource-fds - (make-test-shell-context) (list (list :fd 3) (list :fd 4)))))) (it "source-pipeline-exit-status-honors-pipefail" (expect 2 :to-equal (nshell.application::%source-pipeline-exit-status (list 0 2) nil)) @@ -574,8 +559,7 @@ (declare (ignore ignored-context)) (values commands nil (list :nested)))) ((quote nshell.application::%abort-process-substitution-resources) - (lambda (ignored-context resources) - (declare (ignore ignored-context)) + (lambda (resources) (setf aborted resources)))) (multiple-value-bind (path resource error) (nshell.application::%materialize-process-substitution-in-context @@ -616,8 +600,8 @@ (declare (ignore ignored-context value)) (values nil nil "substitution failed"))) ((quote nshell.application::%abort-process-substitution-resources) - (lambda (ignored-context resources) - (declare (ignore ignored-context resources)) + (lambda (resources) + (declare (ignore resources)) (setf abort-called t)))) (multiple-value-bind (args resources error) (nshell.application::%expand-command-args-in-context context command) @@ -644,8 +628,7 @@ (values command nil (list :resource)) (values nil "node failed" nil)))) ((quote nshell.application::%abort-process-substitution-resources) - (lambda (ignored-context resources) - (declare (ignore ignored-context)) + (lambda (resources) (setf aborted resources)))) (multiple-value-bind (commands error resources) (nshell.application::%expand-command-nodes-in-context @@ -656,8 +639,7 @@ (expect error :to-equal "node failed") (expect aborted :to-equal (list :resource)))))) (it "os-process-substitution-pipeline-releases-resources-after-spawn" - (let ((context (make-test-shell-context)) - (captured nil) + (let ((captured nil) (released nil) (waited nil)) (with-temporary-functions @@ -679,7 +661,6 @@ (push resource waited)))) (multiple-value-bind (output code) (nshell.application::%execute-os-pipeline-with-process-substitutions - context '(:command) nil (list :resource) @@ -690,8 +671,7 @@ (expect released :to-equal (list :resource :resource)) (expect waited :to-equal (list :resource)))))) (it "os-process-substitution-pipeline-aborts-on-spawn-failure" - (let ((context (make-test-shell-context)) - (closed nil)) + (let ((closed nil)) (with-temporary-functions (((quote nshell.infrastructure.acl:process-substitution-resource-fd) (lambda (resource) @@ -706,7 +686,6 @@ (push resource closed)))) (multiple-value-bind (output code) (nshell.application::%execute-os-pipeline-with-process-substitutions - context '(:command) nil (list :resource) @@ -724,8 +703,7 @@ (declare (ignore ignored-context ignored-command)) (values command nil (list :resource)))) ((quote nshell.application::%abort-process-substitution-resources) - (lambda (ignored-context resources) - (declare (ignore ignored-context)) + (lambda (resources) (setf aborted resources)))) (multiple-value-bind (output code) (nshell.application::execute-command-node-in-context context command) @@ -745,8 +723,7 @@ nil (list :resource)))) ((quote nshell.application::%abort-process-substitution-resources) - (lambda (ignored-context resources) - (declare (ignore ignored-context)) + (lambda (resources) (setf aborted resources)))) (multiple-value-bind (output code) (nshell.application::execute-pipeline-node-in-context context pipeline) @@ -765,8 +742,8 @@ nil (list :resource)))) ((quote nshell.application::%execute-os-pipeline-with-process-substitutions) - (lambda (ignored-context commands redirects resources pipefail-p) - (declare (ignore ignored-context commands redirects resources pipefail-p)) + (lambda (commands redirects resources pipefail-p) + (declare (ignore commands redirects resources pipefail-p)) (values "os output" 12)))) (multiple-value-bind (output code) (nshell.application::execute-pipeline-node-in-context context pipeline) diff --git a/t/unit/test-expansion.lisp b/t/unit/test-expansion.lisp index ec70d6d..1782e4c 100644 --- a/t/unit/test-expansion.lisp +++ b/t/unit/test-expansion.lisp @@ -13,10 +13,10 @@ Each case is (EXPECTED INPUT &rest ARGS)." (let ((expander (gensym "EXPANDER-"))) `(let ((,expander ,builder)) - ,@(mapcar (lambda (case) - (destructuring-bind (expected &rest args) case - `(expect (,predicate ',expected (funcall ,expander ,@args)) :to-be-truthy))) - cases)))) + ,@(mapcar (lambda (case) + (destructuring-bind (expected &rest args) case + `(expect (,predicate ',expected (funcall ,expander ,@args)) :to-be-truthy))) + cases)))) (defmacro %assert-expansion-cases-with-env ((predicate expander env-form) &body cases) "Assert CASES against an EXPANDER that takes INPUT and ENV." @@ -25,40 +25,40 @@ Each case is (EXPECTED INPUT &rest ARGS)." (expander-fn (gensym "EXPANDER-"))) `(let ((,env ,env-form) (,expander-fn ,expander)) - (%assert-expansion-cases (,predicate - (lambda (,input) - (funcall ,expander-fn ,input ,env))) - ,@cases)))) + (%assert-expansion-cases (,predicate + (lambda (,input) + (funcall ,expander-fn ,input ,env))) + ,@cases)))) (defmacro %assert-multiple-value-cases ((predicate builder) &body cases) "Assert CASES against a BUILDER that returns multiple values." (let ((expander (gensym "EXPANDER-"))) `(let ((,expander ,builder)) - ,@(mapcar (lambda (case) - (destructuring-bind (expected &rest args) case - `(expect (,predicate ',expected - (multiple-value-list - (funcall ,expander ,@args))) :to-be-truthy))) - cases)))) + ,@(mapcar (lambda (case) + (destructuring-bind (expected &rest args) case + `(expect (,predicate ',expected + (multiple-value-list + (funcall ,expander ,@args))) :to-be-truthy))) + cases)))) (defmacro %assert-quote-style-dispatch-case (style expected branch) `(let ((observed-branch nil)) - (expect ,expected :to-equal (nshell.domain.expansion:expand-by-quote-style - ,style - (progn (setf observed-branch :unquoted) '("unquoted")) - (progn (setf observed-branch :single) '("single")) - (progn (setf observed-branch :double) '("double")))) - (expect ,branch :to-be observed-branch))) + (expect ,expected :to-equal (nshell.domain.expansion:expand-by-quote-style + ,style + (progn (setf observed-branch :unquoted) '("unquoted")) + (progn (setf observed-branch :single) '("single")) + (progn (setf observed-branch :double) '("double")))) + (expect ,branch :to-be observed-branch))) (defmacro %assert-command-name-case (input style expected-command expected-error-count env) `(multiple-value-bind (command error) (nshell.domain.expansion:expand-command-name-by-quote-style ,input ,style ,env) - (expect ,expected-command :to-equal command) - (if ,expected-error-count - (expect (format nil "nshell: ~a: command name expansion produced ~d fields~%" - ,input ,expected-error-count) :to-equal error) - (expect error :to-be-null)))) + (expect ,expected-command :to-equal command) + (if ,expected-error-count + (expect (format nil "nshell: ~a: command name expansion produced ~d fields~%" + ,input ,expected-error-count) :to-equal error) + (expect error :to-be-null)))) (describe "expansion-tests" (it "dollar-var-expansion" @@ -180,17 +180,17 @@ Each case is (EXPECTED INPUT &rest ARGS)." (it "single-command-name-or-error-validates-non-empty-cardinality" "Command-position resolution drops empty fields before validating cardinality." (multiple-value-bind (command error) - (nshell.domain.expansion:single-command-name-or-error "$EMPTY" '("")) + (nshell.domain.expansion::%single-command-name-or-error "$EMPTY" '("")) (expect command :to-be-null) (expect (format nil "nshell: $EMPTY: command name expansion produced 0 fields~%") :to-equal error)) (multiple-value-bind (command error) - (nshell.domain.expansion:single-command-name-or-error "$CMD" '("" "echo" "")) + (nshell.domain.expansion::%single-command-name-or-error "$CMD" '("" "echo" "")) (expect "echo" :to-equal command) (expect error :to-be-null)) (multiple-value-bind (command error) - (nshell.domain.expansion:single-command-name-or-error "$CMD" '("echo" "printf")) - (expect command :to-be-null) - (expect (format nil "nshell: $CMD: command name expansion produced 2 fields~%") :to-equal error))) + (nshell.domain.expansion::%single-command-name-or-error "$CMD" '("echo" "printf")) + (expect command :to-be-null) + (expect (format nil "nshell: $CMD: command name expansion produced 2 fields~%") :to-equal error))) (it "command-name-candidate-resolves-non-empty-cardinality" "command-name-candidate owns empty-field removal before command resolution." diff --git a/t/unit/test-manage-job.lisp b/t/unit/test-manage-job.lisp index 4ad7dd4..88f744f 100644 --- a/t/unit/test-manage-job.lisp +++ b/t/unit/test-manage-job.lisp @@ -30,7 +30,7 @@ (nth-value 1 (find-symbol "MAKE-JOB-LISTING" "NSHELL.APPLICATION"))) :to-be-falsy) (expect (fboundp 'nshell.application::copy-job-listing) :to-be-falsy) - (expect (fboundp (list 'setf 'nshell.application:job-listing-command)) :to-be-falsy) + (expect (fboundp '(setf nshell.application:job-listing-command)) :to-be-falsy) (expect "printf ok" :to-equal (nshell.application:job-listing-command listing)))) (it "bg-marks-job-as-background-and-publishes-continuation" @@ -79,17 +79,9 @@ (job (make-test-job 0 "sleep" :args '("10") :pgid 4321)) (job-id (nshell.domain.job-control:monitor-add-job monitor job)) (set-foreground-calls nil) - (foreground-pgid 0) - (process-fns - (list :get-foreground-pgroup (lambda () 1000) - :set-foreground-pgroup - (lambda (pgid) - (push pgid set-foreground-calls)) - :set-foreground-pgid-state - (lambda (pgid) - (setf foreground-pgid (or pgid 0))))) (nshell.application:*shell-pgid* 1000) - (nshell.application:*foreground-job-pgid* nil)) + (nshell.application:*foreground-job-pgid* nil) + (nshell.infrastructure.acl::*foreground-pgid* 0)) (with-temporary-functions (('nshell.application::%continue-process-group (lambda (pgid) @@ -100,15 +92,19 @@ (expect job-id :to-equal waited-job-id) (expect monitor :to-be waited-monitor) (expect 4321 :to-equal nshell.application:*foreground-job-pgid*) - (expect 4321 :to-equal foreground-pgid) + (expect 4321 :to-equal nshell.infrastructure.acl::*foreground-pgid*) (error "wait failed"))) - ) + ('nshell.infrastructure.acl:get-foreground-pgroup + (lambda () 1000)) + ('nshell.infrastructure.acl:set-foreground-pgroup + (lambda (pgid) + (push pgid set-foreground-calls)))) (handler-case - (nshell.application:fg job-id nil nil nil monitor process-fns) + (nshell.application:fg job-id nil nil nil monitor) (error (condition) (expect (search "wait failed" (princ-to-string condition)) :to-be-truthy))) (expect nshell.application:*foreground-job-pgid* :to-be-null) - (expect 0 :to-equal foreground-pgid) + (expect 0 :to-equal nshell.infrastructure.acl::*foreground-pgid*) (expect '(1000 4321) :to-equal set-foreground-calls)))) (it "fg-publishes-continuation-to-dispatcher" "FG emits a continuation event through the supplied dispatcher." @@ -117,17 +113,9 @@ (job (make-test-job 0 "sleep" :args '("10") :pgid 4321)) (job-id (nshell.domain.job-control:monitor-add-job monitor job)) (set-foreground-calls nil) - (foreground-pgid 0) - (process-fns - (list :get-foreground-pgroup (lambda () 1000) - :set-foreground-pgroup - (lambda (pgid) - (push pgid set-foreground-calls)) - :set-foreground-pgid-state - (lambda (pgid) - (setf foreground-pgid (or pgid 0))))) (nshell.application:*shell-pgid* 1000) - (nshell.application:*foreground-job-pgid* nil)) + (nshell.application:*foreground-job-pgid* nil) + (nshell.infrastructure.acl::*foreground-pgid* 0)) (with-event-capture (continued dispatcher :job-continued) (nshell.domain.events:domain-event-type event) (with-temporary-functions @@ -140,11 +128,15 @@ (expect job-id :to-equal waited-job-id) (expect monitor :to-be waited-monitor) (expect 4321 :to-equal nshell.application:*foreground-job-pgid*) - (expect 4321 :to-equal foreground-pgid) + (expect 4321 :to-equal nshell.infrastructure.acl::*foreground-pgid*) waited-job)) - ) + ('nshell.infrastructure.acl:get-foreground-pgroup + (lambda () 1000)) + ('nshell.infrastructure.acl:set-foreground-pgroup + (lambda (pgid) + (push pgid set-foreground-calls)))) (expect job :to-be - (nshell.application:fg job-id dispatcher nil nil monitor process-fns))) + (nshell.application:fg job-id dispatcher nil nil monitor))) (expect (nshell.application:drain-events dispatcher) :to-be-null) (expect '(1000 4321) :to-equal set-foreground-calls) (expect '(:job-continued) :to-equal (nreverse continued))))) @@ -248,19 +240,19 @@ (let ((nshell.application:*shell-pgid* 1000) (nshell.application:*foreground-job-pgid* nil)) (with-temporary-function - ('nshell.application::%get-foreground-pgroup + ('nshell.infrastructure.acl:get-foreground-pgroup (lambda () 1000)) (expect (nshell.application::%foreground-signal-target-pgid) :to-be-null))) (let ((nshell.application:*shell-pgid* 1000) (nshell.application:*foreground-job-pgid* 1000)) (with-temporary-function - ('nshell.application::%get-foreground-pgroup + ('nshell.infrastructure.acl:get-foreground-pgroup (lambda () 2000)) (expect (nshell.application::%foreground-signal-target-pgid) :to-be-null))) (let ((nshell.application:*shell-pgid* 1000) (nshell.application:*foreground-job-pgid* nil)) (with-temporary-function - ('nshell.application::%get-foreground-pgroup + ('nshell.infrastructure.acl:get-foreground-pgroup (lambda () 2000)) (expect 2000 :to-equal (nshell.application::%foreground-signal-target-pgid))))) @@ -313,7 +305,7 @@ (calls nil)) (nshell.domain.job-control:background-job monitor job-id) (with-temporary-function - ((quote nshell.application::%kill-process) + ((quote nshell.infrastructure.acl:kill-process) (lambda (pid signal) (push (list pid signal) calls) 0)) @@ -379,14 +371,14 @@ (it "classifies wait errors without an OS wait" "Known wait errors become application events while unknown errors remain unclassified." (let* ((no-child - (nshell.application::%classify-job-wait-error :echild)) + (nshell.application::%classify-job-wait-error sb-posix:echild)) (interrupted - (nshell.application::%classify-job-wait-error :eintr)) + (nshell.application::%classify-job-wait-error sb-posix:eintr)) (unknown-errno - (nshell.application::%classify-job-wait-error :enoent))) + (1+ (max sb-posix:echild sb-posix:eintr)))) (expect :no-child :to-be (nshell.application::job-wait-event-state no-child)) (expect :interrupted :to-be (nshell.application::job-wait-event-state interrupted)) (expect nil :to-equal - unknown-errno)))) + (nshell.application::%classify-job-wait-error unknown-errno)))))