• mina86.com

  • Categories
  • Code
  • Contact
  • Reading stdin with Emacs Client

    Posted by Michał ‘mina86’ Nazarewicz on 21st of February 2021

    One feature Emacs doesn’t have out of the box is reading data from standard input. Trying to open - (e.g. echo stdin | emacs -) results in Emacs complaining about unknown option (if it ends up starting in graphical mode) or that ‘standard input is not a tty’ (when starting in terminal).

    With sufficiently advanced shell one potential solution is the --insert flag paired with command substitution: echo stdin | emacs --insert <(cat). Sadly, it’s not a panacea. It messes up initial buffer (and thus may break setups with custom initial-buffer-choice) and doesn’t address the issue of standard input not being a tty when running Emacs in terminal.

    For me the biggest problem though is that it isn’t available when using emacsclient. Fortunately, as previously mentioned the Emacs Server protocol allows for far more than just instructions to open a file. Indeed, my solution to the problem revolves around the use of --eval option:

    #!/usr/bin/perl
    
    use strict;
    use warnings;
    
    my @args = @ARGV;
    if (!@args) {
    	my $data;
    	$data = join '', <STDIN>;
    	$data =~ s/\\/\\\\/g;
    	$data =~ s/"/\\"/g;
    	$data = <<ELISP;
    (let ((buf (generate-new-buffer "*stdin*")))
      (switch-to-buffer buf)
      (insert "$data")
      (goto-char (point-min))
      (x-focus-frame nil)
      (buffer-name buf))
    ELISP
    	@args = ('-e', $data);
    }
    
    exec 'emacsclient', @args;
    die "emacsclient: $!\n";

    People allergic to Perl may find this Python version more palatable:

    #!/usr/bin/python3
    
    import os
    import re
    import sys
    
    args = sys.argv[1:]
    if not args:
            data = sys.stdin.read()
            data = data.replace('\\', '\\\\').replace('"', '\\"')
            data = ('(let ((buf (generate-new-buffer "*stdin*")))'
                    '(switch-to-buffer buf)'
                    '(insert "' + data + '")'
                    '(goto-char (point-min))'
                    '(x-focus-frame nil)'
                    '(buffer-name buf))')
            args = ('-e', data)
    
    os.execlp('emacsclient', 'emacsclient', *args)

    Usage is straightforward. When called without arguments, the script will read standard input and insert it’s content into a fresh *pipe* Emacs buffer. When called with arguments, they are passed unchanged to emacsclient.

    This isn’t the only approach of course and it is limited to how large the input can be. On Linux the maximum it can cope is roughly 131 kB. That should be plenty for many use cases but it case it’s not a different way to deal with the issue would be to store standard input in a temporary file. While it may even be more efficient, I often use Emacs from remote hosts and not having to deal with path name confusion is an advantage of the above scripts.