reddit / emacs

Help with creating async Consult command with pipes

whirlwindlatitude · 2022-02-09 13:49
Here's what I'm trying to accomplish, in case there's a better way than my current plan: I want to search the full text of the files for **all** the terms of the input (terms separated by spaces), no matter where in the file they are or in what order. So, if I search for "apple banana", I want to find a file where "banana" is on line 1 and "apple" is on line 15. Since pretty much all search tools are line-oriented and/or keep the order of the terms and/or use 'OR' matching instead of 'AND' matching, I came up with a way to do what I want via a series of pipes. Something like: rg --ignore-case --files-with-matches --color=never --null . -e 'apple' | xargs -0 rg --ignore-case --files-with-matches --color=never -e 'banana' I have a builder function that will output the correct command according to the input, but for the sake of example let's say it's a static function like so: (defun my-builder (input) '("rg" "--ignore-case" "--files-with-matches" "--color=never" "--null" "." "-e" "'apple'" "|" "xargs" "-0" "rg" "--ignore-case" "--files-with-matches" "--color=never" "-e" "'banana'")) The string version of this command works perfectly in the command line. But when I try to use the builder with a Consult command: (defun my-consult (&optional dir initial) (interactive "P") (find-file (consult--read (consult--async-command #'my-builder (consult--async-map (lambda (x) (string-remove-prefix "./" x)))) :prompt "Hey: " :sort nil :require-match t :initial (consult--async-split-initial initial) :add-history (consult--async-split-thingatpt 'filename) :category 'file :history '(:input consult--find-history)))) ... It fails. The log reads: consult--async-process sentinel: event=exited abnormally with code 2 lines=0 >>>>> stderr >>>>> |: No such file or directory (os error 2) xargs: No such file or directory (os error 2) rg: No such file or directory (os error 2) <<<<< stderr <<<<< It seems `make-process`, which Consult uses, doesn't like to have a complex line with pipes in it. (The documentation says the first item of the command list is the program name, and the rest are args -- but of course, "|" and the rest are not args for rg. I *think* that's where the problem is, but I'm not really sure.) So. Does anyone know of a workaround? Or is there a better way to accomplish what I'm after? Any help appreciated. Thanks!
_viz_ · 2022-02-10 06:05
Pipe is a shell construct so either use it, or see if rg already supports what you need (I, for one, for would be surprised if it didn't already). EDIT: But from the reddit comments I read, you are doing something that is unneeded. Nothing is stopping you from further filtering the matches using your completion mechanism
whirlwindlatitude · 2022-02-10 11:47
Ah, you're right, of course... Counsel, for instance, uses `start-file-process-shell-command` instead of `make-process`, which actually delegates to a shell and allows for the pipe. As far as I can tell, ripgrep unfortunately does not support this natively. It does have multiline support, but it's only for *patterns* that are multiline. > Nothing is stopping you from further filtering the matches using your completion mechanism Do you have any idea how to do this further filtering? Is there a way to easily chain filters? (I'm using `fido-vertical-mode`, which is really `icomplete`.) Just asking in case you have some experience in this area already, but I'll dig further. Thanks for the help!
_viz_ · 2022-02-10 13:30
> Do you have any idea how to do this further filtering? Is there a way to easily chain filters? (I'm using fido-vertical-mode, which is really icomplete.) Just asking in case you have some experience in this area already, but I'll dig further. No idea, sorry. I don't use consult and friends.
whirlwindlatitude · 2022-02-10 19:51
Thanks to /u/\_viz\_'s comment, it became clear that if I wanted to execute a complex shell command then, well, I would need to use a shell. So I patched Consult's `consult--async-process` to do just that when needed. It's a very hacky thing and I'm not even sure if it'll behave correctly in all cases, BUT it works for for me! Here's the patched function (using `el-patch`). It's a long function, so search for "THE PATCH" to get to the good stuff: (el-patch-feature consult) (el-patch-defun consult--async-process (async cmd &rest props) "Create process source async function. ASYNC is the async function which receives the candidates. CMD is the command line builder function. PROPS are optional properties passed to `make-process'." (let (proc proc-buf last-args indicator count) (lambda (action) (pcase action ("" ;; If no input is provided kill current process (when proc (delete-process proc) (kill-buffer proc-buf) (setq proc nil proc-buf nil)) (setq last-args nil)) ((pred stringp) (funcall async action) (let* ((args (funcall cmd action)) (flush t) (rest "") (proc-filter (lambda (_ out) (when flush (setq flush nil) (funcall async 'flush)) (let ((lines (split-string out "[\r\n]+"))) (if (not (cdr lines)) (setq rest (concat rest (car lines))) (setcar lines (concat rest (car lines))) (let* ((len (length lines)) (last (nthcdr (- len 2) lines))) (setq rest (cadr last) count (+ count len -1)) (setcdr last nil) (funcall async lines)))))) (proc-sentinel (lambda (_ event) (when flush (setq flush nil) (funcall async 'flush)) (overlay-put indicator 'display (consult--process-indicator event)) (when (and (string-prefix-p "finished" event) (not (string= rest ""))) (setq count (+ count 1)) (funcall async (list rest))) (consult--async-log "consult--async-process sentinel: event=%s lines=%d\n" (string-trim event) count) (with-current-buffer (get-buffer-create consult--async-log) (goto-char (point-max)) (insert ">>>>> stderr >>>>>\n") (insert-buffer-substring proc-buf) (insert "<<<<< stderr <<<<<\n"))))) (unless (equal args last-args) (setq last-args args) (when proc (delete-process proc) (kill-buffer proc-buf) (setq proc nil proc-buf nil)) (when args (overlay-put indicator 'display #("*" 0 1 (face consult-async-running))) (consult--async-log "consult--async-process started %S\n" args) ;; HERE'S THE PATCH: (el-patch-swap ;; OLD FORM: (setq count 0 proc-buf (generate-new-buffer " *consult-async-stderr*") proc (apply #'make-process `(,@props :connection-type pipe :name ,(car args) ;;; XXX tramp bug, the stderr buffer must be empty :stderr ,proc-buf :noquery t :command ,args :filter ,proc-filter :sentinel ,proc-sentinel))) ;; NEW FORM: (progn (setq count 0 proc-buf (generate-new-buffer " *consult-async-stderr*")) (if (string= (car (last args)) "@@@") (progn (setq proc (start-file-process-shell-command (car args) nil (string-join (butlast args) " "))) (set-process-sentinel proc proc-sentinel) (set-process-filter proc proc-filter)) (setq proc (apply #'make-process `(,@props :connection-type pipe :name ,(car args) ;;; XXX tramp bug, the stderr buffer must be empty :stderr ,proc-buf :noquery t :command ,args :filter ,proc-filter :sentinel ,proc-sentinel))))))))) nil) ('destroy (when proc (delete-process proc) (kill-buffer proc-buf) (setq proc nil proc-buf nil)) (delete-overlay indicator) (funcall async 'destroy)) ('setup (setq indicator (make-overlay (- (minibuffer-prompt-end) 2) (- (minibuffer-prompt-end) 1))) (funcall async 'setup)) (_ (funcall async action)))))) The patched function recognizes if the last element of the command list is "@@@". If so, it'll concat it into a string and pass it to `start-file-process-shell-command`, setting up the same sentinel and filter. (I kinda ignored the other options that Consult passes to `make-process` and hoped they wouldn't matter too much. It worked, so...) My builder function just has to append "@@@" to the command list and now I can use my pipes! Here's the real builder function: (defun my-builder (input) (let* ((common (split-string "rg --ignore-case --files-with-matches --color=never --max-depth=1")) (query (split-string input)) (cmd (if (= (length query) 1) (append common '("." "-e") query) (flatten-tree (list common "--null" "." "-e" (car query) (mapcar (lambda (term) (list "|" "xargs" "-0" common "--null" "-e" term)) (butlast (cdr query))) "|" "xargs" "-0" common "-e" (last query)))))) (append cmd '("@@@")))) As I said, this is very hacky, but it's a very small change to Consult that does what I need. So I'm happy.