#!/usr/bin/env ruby require 'socket' XDG_RUNTIME_DIR = ENV['XDG_RUNTIME_DIR'] XDG_CONFIG_HOME = ENV['XDG_CONFIG_HOME'] HOME = ENV['HOME'] TMPDIR = ENV['TMPDIR'] APPDATA = ENV['APPDATA'] UID = Process.uid DEFAULT_PATHS = [ # unix socket ENV['EMACS_SOCKET_NAME'], XDG_RUNTIME_DIR && "#{XDG_RUNTIME_DIR}/emacs/server", TMPDIR && "#{TMPDIR}/emacs#{UID}/server", "/tmp/emacs#{UID}/server", # tcp config ENV['EMACS_SERVER_FILE'], HOME && XDG_CONFIG_HOME && "#{HOME}/#{XDG_CONFIG_HOME}/emacs/server/server", HOME && "#{HOME}/.config/emacs/server/server", HOME && "#{HOME}/.emacs.d/server/server", APPDATA && XDG_CONFIG_HOME && "#{APPDATA}/#{XDG_CONFIG_HOME}/emacs/server/server", APPDATA && "#{APPDATA}/.config/emacs/server/server", APPDATA && "#{APPDATA}/.emacs.d/server/server" ].compact.freeze MAX_MESSAGE_SIZE = 1024 def die(msg) $stderr.puts(msg) exit(1) end def detect_path DEFAULT_PATHS.find { |path| File.exist?(path) } end def parse_tcp_config(path) # : # meta, secret = File.open(path, &:readlines) host, rest = meta.split(':') port, pid = rest.split(' ') { host: host, port: port.to_i, pid: pid.to_i, secret: secret } end def quote(arg) # & -> &&, - -> &-, RET -> &n, SPC -> &_ arg.to_s.gsub(/[-&\n ]/, '-' => '&-', '&' => '&&', "\n" => '&n', ' ' => '&_') end def unquote(arg) # && -> &, &- -> -, &n -> RET, &_ -> SPC arg.to_s.gsub(/&[-&n_]/, '&-' => '-', '&&' => '&', '&n' => "\n", '&_' => ' ') end def read_untilc(io, char) out = io.gets(char) return unless out out.chomp(char) end def read_socket(socket) output = '' loop do command = read_untilc(socket, ' ') case command when '-emacs-pid' then read_untilc(socket, "\n") when '-print' then output = unquote(read_untilc(socket, "\n")) when '-print-nonl' then output += unquote(read_untilc(socket, "\n")) # HACK: in case of an error, the remote side neither terminates # the argument with a newline nor signals an EOF immediately, but # waits for 5s until shutting down, so the remaining buffered # content must be read to avoid blocking for 5s when '-error' then raise unquote(socket.readpartial(MAX_MESSAGE_SIZE)) when nil then break else raise "unknown command: #{command}" end end output end def unix_eval(path, code) Socket.unix(path) do |socket| socket.puts("-eval #{quote(code)}") read_socket(socket) end end def tcp_eval(path, code) config = parse_tcp_config(path) Socket.tcp(config[:host], config[:port]) do |socket| linger = [1, 1].pack('ii') # terrible, terrible hack socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, linger) socket.puts("-auth #{config[:secret]} -eval #{quote(code)}") read_socket(socket) end end def eval_code(code) path = ENV['EMACSCLIENT_FILE'] || detect_path || die("couldn't detect path") File.stat(path).socket? ? unix_eval(path, code) : tcp_eval(path, code) end die("usage: #{$PROGRAM_NAME} ") unless ARGV.count == 1 puts eval_code(ARGV[0])