What is wrong with my string interpolation?

I need to pass lisp to emacs from the terminal with an alias, but the alias is not populating the way I would like it to in the shell configuration.

I am trying to set a shell alias in home-manager like this:

{ pkgs, lib, config, ... }:
let
  emacsCommand = headline: ''
    emacsclient -t --eval='(progn (find-file "~/nixfiles/README.org") (goto-char (org-find-exact-headline-in-buffer \\"${headline}\\")))'
  '';
in
  {
  home.shellAliases = {
# ...
  cfb = emacsCommand "Browsers";
  cfw = "nvim ~/nixfiles/modules/home-manager/default.nix";
# ...
}

But when I check the ~/.config/config.fish, the alias is set to this:

alias cfb 'emacsclient -t --eval='\''(progn (find-file "~/nixfiles/README.org") (goto-char (org-find-exact-headline-in-buffer "Browsers")))'\'''

I want the result to be this:

alias cfb 'emacsclient -t --eval=\'(progn (find-file "~/nixfiles/README.org") (goto-char (org-find-exact-headline-in-buffer "Browsers")))\''

I have been playing in the repl for an hour and I cannot make it happen. What am I doing wrong?

Other things tried

nix-repl> let 
              emacsCommand = headline: ''
            emacsclient -t --eval='(progn (find-file "~/nixfiles/README.org") (goto-char (org-find-exact-headline-in-buffer "${headline}")))';
            '';
          in emacsCommand "Browsers"

results in "emacsclient -t --eval='(progn (find-file \"~/nixfiles/README.org\") (goto-char (org-find-exact-headline-in-buffer \"Browsers\")))';\n"

When I put that in the config, the config.fish:

alias cfb 'emacsclient -t --eval='\''(progn (find-file "~/nixfiles/README.org") (goto-char (org-find-exact-headline-in-buffer "Browsers")))'\'';

I have read this bit about string interpolation from the manual, but it looks like home-manager is automatically adding in more escapes than I am telling it to do, so I can’t figure out how to get it to do what I want.

Indeed, it automatically shell-escapes the value

Seems correct to me, it’s closing the string, then putting a single quote literal, then starting the string again (at least what I see, I doh’t use fish, just extrapolating from bash here).

Thanks for that. Do you know how I can avoid that? If I put the verbatim with no escapes, it’s incorrect:

emacsclient -t --eval='(progn (find-file "~/nixfiles/README.org") (goto-char (org-find-exact-headline-in-buffer "${headline}")))';

becomes

alias cfb 'emacsclient -t --eval='\''(progn (find-file "~/nixfiles/README.org") (goto-char (org-find-exact-headline-in-buffer "Browsers")))'\'';

and gives me this error because now it’s putting single and double quotes around the lisp (I think, I am new to lisp):

$ cfb
/nix/store/2m9cfl49avfj5irb
jzpy96y2nvx834n9-emacs-29.4/bin/emacsclient: option '--eval' doesn't allow an argument
Try '/nix/store/2m9cfl49avfj5irbjzpy96y2nvx834n9-emacs-29.4/bin/emacsclient --help' for more information
- (line 2): The expanded command was empty.
 $argv
 ^~~~^
in function 'cfb'

It should be \'(progn (find-file "~/nixfiles/README.org") (goto-char (org-find-exact-headline-in-buffer "Browsers")))\'

Honestly, when escaping gets this messy I just wouldn’t bother with shellAliases. I’d just use something like programs.bash.initExtra so that you can just manually do it correctly.

Yeah, I was thinking about that, but I was trying to set it up to work with any shell :frowning:

Escaping user input without being told to or without giving an option to turn that off is questionable design, imo. I don’t know enough nix to do a pull request about it though

The documentation for home.shellAliases says:

This option should only be used to manage simple aliases that are compatible across all shells. If you need to use a shell specific feature then make sure to use a shell specific option, for example programs.bash.shellAliases for Bash.

The command you’re emitting is not a simple alias compatible across all shells, as you’re discovering. If you use programs.fish.shellAliases, you should get escaping that is correct for the fish shell. (That’s a moral ‘should’; I haven’t tested it, but if it doesn’t work that’s a bug.)

If you want something complex that works in any shell you should probably just put an executable on PATH rather than trying to make an alias. Like put a writeShellScriptBin in your packages list.

I did read that, and I disagree with your take. This is a very straightforward alias that should be compatible across all shells. I am simply passing an option to emacs, which involves lisp, which involves single quotes that aren’t escaped automatically. I simply want the string I am typing to be sent to the shell I am typing the alias in.

I understand what you want, but the alias can’t be expressed in the same form in both shells. The result alias cfb 'emacsclient -t --eval=\'... is not valid in Bash, and the result alias cfb 'emacsclient -t --eval='\''... is not valid in fish. They have incompatible escaping rules, which means that if your alias uses single quotes, it isn’t ‘simple’ in the sense intended by the documentation. It may seem simple to you anyway but no amount of wishing on your part changes the fundamental incompatibility of escaping rules between shells.

3 Likes

You make sense. I guess I had thought the “complex” it was talking about, for example, functions and how they change between fish, nushell, and bash.

Thanks very much for your help

1 Like