Something went wrong. Try again.
Runabout is a long-term agent harness.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124(in-package #:runabout.repl)
(defun bytes (path &key (offset 0) (count 256) (width 16)) "Return hexdump rows for a window of PATH: plists with :OFFSET, :HEX and :ASCII. OFFSET is the first byte shown (default 0), COUNT how many bytes (default 256), WIDTH bytes per row (default 16). A window past the end of the file returns no rows; one that runs past the end is clamped to it. The second value is the bytes remaining after the window, the third the file's size in bytes. Only the window is read, so a large file costs its rows, not its length." (check-type offset (integer 0 *)) (check-type count (integer 0 *)) (check-type width (integer 1 *)) (let ((stat (ignore-errors (sb-posix:stat path)))) (when (and stat (sb-posix:s-isdir (sb-posix:stat-mode stat))) (fail "BYTES reads a file's bytes; ~a is a directory" path))) (with-open-file (in path :element-type '(unsigned-byte 8)) (let* ((size (file-length in)) (from (min offset size)) (to (min size (+ from count)))) (file-position in from) (let ((window (make-array (- to from) :element-type '(unsigned-byte 8)))) (read-sequence window in) (values (loop for start from 0 below (length window) by width for end = (min (length window) (+ start width)) collect (list :offset (+ from start) :hex (format nil "~{~2,'0x~^ ~}" (coerce (subseq window start end) 'list)) :ascii (map 'string (lambda (byte) (if (<= 32 byte 126) (code-char byte) #\.)) (subseq window start end)))) (- size to) size)))))
(defun regex-quote (text) (with-output-to-string (out) (loop for c across text do (when (find c "\\.^$|?*+()[]{}") (write-char #\\ out)) (write-char c out))))
(defun grep (pattern path &key ignore-case fixed word line invert (recursive t) (before 0) (after 0) context limit include exclude) "Search PCRE2 regexes; return (:PATH :LINE :TEXT :MATCH) rows and match count.Case-sensitive by default; :FIXED T searches literal text. :WORD / :LINE matchwhole words / lines; :INVERT selects nonmatches. PATH is a file, directory, orlist of paths. :RECURSIVE NIL searches only immediate directory entries.Binary files and recursive symlinks are skipped.:BEFORE / :AFTER add context; :CONTEXT sets both. Context rows have :MATCH NIL.:LIMIT caps total matches (NIL means unlimited). :INCLUDE / :EXCLUDE acceptfilename globs or lists of globs. Regex backslashes must be doubled in Lisp strings." (check-type before (integer 0 *)) (check-type after (integer 0 *)) (check-type context (or null (integer 0 *))) (check-type limit (or null (integer 0 *))) (when context (setf before context after context)) (let* ((pattern (if fixed (regex-quote pattern) pattern)) (pattern (cond (line (format nil "\\A(?:~a)\\z" pattern)) (word (format nil "(?<!\\w)(?:~a)(?!\\w)" pattern)) (t pattern))) (hits 0) (rows '())) (call-with-regex pattern ignore-case (lambda (matches) (labels ((wanted (name) (flet ((glob (patterns) (some (lambda (p) (zerop (%filename-match p name 0))) (if (listp patterns) patterns (list patterns))))) (and (or (null include) (glob include)) (not (and exclude (glob exclude)))))) (scan (path stat) (when (and limit (>= hits limit)) (throw 'search-done nil)) (when (and (eq :file (stat-type stat)) (wanted (file-name-part path))) (let* ((text (handler-case (uiop:read-file-string path) (sb-int:stream-decoding-error () nil))) (lines (when (and text (not (find #\Null text))) (coerce (mapcar #'car (file-lines (without-bom text))) 'vector))) (n (length lines)) (selected (make-array n :initial-element nil)) (matched (make-array n :initial-element nil))) (loop for i below n while (or (null limit) (< hits limit)) when (if invert (not (funcall matches (aref lines i))) (funcall matches (aref lines i))) do (incf hits) (setf (aref matched i) t) (loop for j from (max 0 (- i before)) below (min n (+ i after 1)) do (setf (aref selected j) t))) (loop for i below n when (aref selected i) do (push (list :path path :line (1+ i) :text (aref lines i) :match (aref matched i)) rows)))))) (catch 'search-done (dolist (item (path-list path)) (let* ((root (namestring (truename item))) (stat (followed-stat root))) (if (eq :directory (stat-type stat)) (walk-tree root #'scan :recursive recursive) (scan root stat)))))))) (values (nreverse rows) hits)))
(defun peek (value &key (lines 40) (start 0) (chars 4000)) "Return a slice: strings by line (capped at CHARS), sequences by element.START is zero-based. Further values count remaining lines/elements and cut characters." (check-type start (integer 0 *)) (check-type lines (integer 0 *)) (check-type chars (integer 0 *)) (if (stringp value) (let ((total 0) (from nil) (to 0)) ;; Keep indices into the original string, never a copy of every line. (loop for begin = 0 then (1+ end) for end = (position #\Newline value :start begin) do (when (<= start total (1- (+ start lines))) (unless from (setf from begin)) (setf to (or end (length value)))) (incf total) while end) (let* ((from (or from to)) (size (- to from))) (values (subseq value from (+ from (min chars size))) (max 0 (- total (+ start lines))) (max 0 (- size chars))))) (let ((n (and (typep value 'sequence) (ignore-errors (if (listp value) (list-length value) (length value)))))) (if n (let ((from (min start n)) (to (min n (+ start lines)))) (values (subseq value from to) (- n to))) (print-limited value chars)))))