fixes for while :

ober

85c092277b19c5212f9dbeb21657a51217ec9f28

diff --git a/.build.yml b/.build.yml
index 12f9bd9..b6b8487 100644
--- a/.build.yml
+++ b/.build.yml
@@ -14,6 +14,7 @@ artifacts:
   - jerboa-shell/jerboa-shell-0.2.0.jpkg
 tasks:
   - build-jerboa-tools: |
+      umask 022
       cd jerboa
       sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
       export CARGO_BUILD_JOBS=1
@@ -26,6 +27,7 @@ tasks:
       cd jerboa-shell
       JERBUILD=$HOME/jerboa/dist/jerbuild make test
   - package: |
+      umask 022
       cd jerboa-shell
       J="$HOME/jerboa/dist/jerboa"
       mkdir -p "$HOME/.cache" && chmod -R go-w "$HOME/.cache"
diff --git a/main.ss b/main.ss
index 9569df8..93838d8 100644
--- a/main.ss
+++ b/main.ss
@@ -612,6 +612,10 @@
                      (parse-complete-command input (env-shopt? env "extglob") alias-fn))))])
        (cond
          ((eq? cmd 'error) 2)
+         ((eq? cmd 'need-more)
+          ;; Incomplete command (e.g., "while :" without "do")
+          (fprintf (current-error-port) "jsh: syntax error: unexpected end of file~n")
+          2)
          ((not cmd) 0)
          (else (execute-command cmd env))))))))
 
diff --git a/parser.ss b/parser.ss
index 3879b12..fcdbc59 100644
--- a/parser.ss
+++ b/parser.ss
@@ -23,20 +23,28 @@
 
 ;; Parse a complete command from input string
 ;; Returns an AST node (command-list, and-or-list, etc.) or #f for empty input
+;; Returns 'need-more if the command is incomplete (e.g., "while :" without "do")
 (def (parse-complete-command input (extglob? #f) (alias-fn #f))
   (let* ((lex (if (string? input) (make-shell-lexer input extglob?) input))
          (ps (make-parser-state lex #f #f [] alias-fn)))
-    (let ((result (parse-list ps)))
-      (when (lexer-needs-more? lex)
-        (set! (parser-state-needs-more? ps) #t))
-      ;; Check for unconsumed tokens — syntax error if non-NEWLINE/EOF remain
-      (let ((tok (parser-peek ps)))
-        (when (and (token? tok) (not (eq? (token-type tok) 'NEWLINE)))
-          (error #f (string-append "parse error near `"
-                                (if (token-value tok) (token-value tok)
-                                    (symbol->string (token-type tok)))
-                                "'"))))
-      result)))
+    (with-catch
+      (lambda (e)
+        ;; Check if this is an incomplete command that needs more input
+        (if (or (lexer-want-more? lex) (parser-state-needs-more? ps))
+          'need-more
+          (raise e)))
+      (lambda ()
+        (let ((result (parse-list ps)))
+          (when (lexer-needs-more? lex)
+            (set! (parser-state-needs-more? ps) #t))
+          ;; Check for unconsumed tokens — syntax error if non-NEWLINE/EOF remain
+          (let ((tok (parser-peek ps)))
+            (when (and (token? tok) (not (eq? (token-type tok) 'NEWLINE)))
+              (error #f (string-append "parse error near `"
+                                    (if (token-value tok) (token-value tok)
+                                        (symbol->string (token-type tok)))
+                                    "'"))))
+          result)))))
 
 ;; Parse one "line" — semicolon-separated commands, stopping at newline.
 ;; This allows execution between lines so shopt changes take effect.
@@ -142,6 +150,10 @@
     (unless (and (token? tok)
                  (eq? (token-type tok) 'WORD)
                  (string=? (token-value tok) word))
+      ;; If we got eof, mark that we need more input (for multi-line compound commands)
+      (when (eq? tok 'eof)
+        (set! (parser-state-needs-more? ps) #t)
+        (set! (lexer-want-more? (parser-state-lexer ps)) #t))
       (error #f (format "parse error: expected '~a', got ~a"
                      word (if (token? tok) (token-value tok) tok))))))
 
diff --git a/script.ss b/script.ss
index dff4753..df5af79 100644
--- a/script.ss
+++ b/script.ss
@@ -88,27 +88,35 @@
   (let ((lines (string-split input #\newline)))
     (let line-loop ((remaining-lines lines) (status 0) (shell-buffer '()))
       (cond
-        ;; No more lines - execute any pending shell commands
-        ((null? remaining-lines)
-         (if (null? shell-buffer)
-           status
-           (let ((shell-input (string-join (reverse shell-buffer) "\n")))
-             (execute-shell-lines shell-input env interactive? status))))
-        ;; Comma meta-command line - execute pending shell input first, then
-        ;; dispatch through the shared meta-command registry.
-        ((and (> (string-length (car remaining-lines)) 0)
-              (char=? (string-ref (car remaining-lines) 0) #\,))
-         (let* ((pending-status
-                 (if (null? shell-buffer)
-                   status
-                   (let ((shell-input (string-join (reverse shell-buffer) "\n")))
-                     (execute-shell-lines shell-input env interactive? status))))
-                (line (car remaining-lines))
-                (meta-status
-                 (execute-meta-command
-                   (substring line 1 (string-length line)))))
-           (env-set-last-status! env meta-status)
-           (line-loop (cdr remaining-lines) meta-status '())))
+         ;; No more lines - execute any pending shell commands
+         ((null? remaining-lines)
+          (if (null? shell-buffer)
+            status
+            (let ((shell-input (string-join (reverse shell-buffer) "\n")))
+              (let ((result (execute-shell-lines shell-input env interactive? status)))
+                ;; Check for incomplete command at end of script
+                (if (eq? result 'need-more)
+                  (begin
+                    (fprintf (current-error-port)
+                             "jsh: syntax error: unexpected end of file~n")
+                    2)
+                  result)))))
+         ;; Comma meta-command line - execute pending shell input first, then
+         ;; dispatch through the shared meta-command registry.
+         ((and (> (string-length (car remaining-lines)) 0)
+               (char=? (string-ref (car remaining-lines) 0) #\,))
+          (let* ((pending-result
+                  (if (null? shell-buffer)
+                    status
+                    (let ((shell-input (string-join (reverse shell-buffer) "\n")))
+                      (execute-shell-lines shell-input env interactive? status))))
+                 (pending-status (if (eq? pending-result 'need-more) 2 pending-result))
+                 (line (car remaining-lines))
+                 (meta-status
+                  (execute-meta-command
+                    (substring line 1 (string-length line)))))
+            (env-set-last-status! env meta-status)
+            (line-loop (cdr remaining-lines) meta-status '())))
         ;; Regular shell line - accumulate
         (else
          (line-loop (cdr remaining-lines) status (cons (car remaining-lines) shell-buffer)))))))
@@ -117,20 +125,27 @@
 (def (execute-shell-lines input env interactive? initial-status)
   (let ((lexer (make-shell-lexer input (env-shopt? env "extglob"))))
     (let loop ((status initial-status))
-      (let ((cmd (with-catch
-                  (lambda (e)
-                    (fprintf (current-error-port) "jsh: syntax error: ~a~n"
-                             (exception-message e))
-                    'error)
-                  (lambda ()
-                    ;; Update lexer extglob flag in case shopt changed it
-                    (set! (lexer-extglob? lexer) (env-shopt? env "extglob"))
-                    ;; Build alias lookup: checks expand_aliases shopt, returns value or #f
-                    (let ((alias-fn (and (env-shopt? env "expand_aliases")
-                                        (lambda (word) (alias-get env word)))))
-                      (parse-one-line lexer (env-shopt? env "extglob") alias-fn))))))
+       (let ((cmd (with-catch
+                    (lambda (e)
+                      ;; Check if lexer needs more input (e.g., incomplete compound command)
+                      (if (lexer-want-more? lexer)
+                        'need-more
+                        (begin
+                          (fprintf (current-error-port) "jsh: syntax error: ~a~n"
+                                   (exception-message e))
+                          'error)))
+                   (lambda ()
+                     ;; Update lexer extglob flag in case shopt changed it
+                     (set! (lexer-extglob? lexer) (env-shopt? env "extglob"))
+                     ;; Build alias lookup: checks expand_aliases shopt, returns value or #f
+                     (let ((alias-fn (and (env-shopt? env "expand_aliases")
+                                         (lambda (word) (alias-get env word)))))
+                       (parse-one-line lexer (env-shopt? env "extglob") alias-fn))))))
         (cond
           ((eq? cmd 'error) 2)  ;; syntax error
+          ((eq? cmd 'need-more)
+           ;; Incomplete command (e.g., "while :" without "do") — signal need for more input
+           'need-more)
           ((not cmd) status)     ;; end of input
           ;; Unterminated quote/construct after parsing — syntax error
           ((lexer-want-more? lexer)
diff --git a/support/adversarial-corpus-evidence.ss b/support/adversarial-corpus-evidence.ss
index 87f2ff3..e6dccf3 100644
--- a/support/adversarial-corpus-evidence.ss
+++ b/support/adversarial-corpus-evidence.ss
@@ -38,7 +38,11 @@
 
 (define (try-parse source extglob?)
   (guard (exn (else (list 'error exn)))
-    (list 'ok (parse-complete-command source extglob? #f))))
+    (let ((result (parse-complete-command source extglob? #f)))
+      ;; Treat incomplete commands (need-more) as parse errors
+      (if (eq? result 'need-more)
+        (list 'error 'incomplete-command)
+        (list 'ok result)))))
 
 (define (sum-list xs f)
   (let loop ((xs xs) (n 0))