Background

Recently I found myself writing a bunch of search and replace one-liners.

1export FROM='Matrix3d'; export TO='Matrix3S'; ag -l $FROM | xargs -I {} sd $FROM $TO {}

Which works, especially since both ag and sd are rather good, but it is still:

  • Slightly non-ergonomic to type
  • Difficult to keep track of
    • Modulo dumping everything in a .sh file

These reminded me of the rich set of alternate shells1.

Reaching for xonsh

Although nushell, elvish and even oil seemed promising, I settled on the Python based xonsh.

1pipx install xonsh[full]
2pipx inject xonsh xcontrib-sh
3echo 'xontrib load sh' >> ~/.xonshrc

Where we use the injection mechanism to add the xcontrib-sh plugin for running shell commands without using xonsh, since we might not want to translate every command to be compliant with xonsh.

Additions

  • xonsh-mode works well with emacs and is on MELPA
  • xxh makes it pretty trivial to take into foreign SSH machines

Scripting substitutions

Directly leveraging xonsh we can rewrite the earlier bash script into:

 1from pathlib import Path
 2
 3def replace_make_shared():
 4    FROM_MAKE = 'Matter'
 5    TO_MAKE = 'Matter'
 6
 7    files = $(ag -l @(FROM_MAKE)).splitlines()
 8
 9    for f in files:
10        if Path(f).exists():
11            sd @(FROM_MAKE) @(TO_MAKE) @(f)
12        else:
13            echo "File not found: @(file)"
14
15replace_make_shared()

Leveraging Python

It is instructive to recall how this would work in pure python.

 1import subprocess
 2from pathlib import Path
 3
 4def replace_make_shared():
 5    FROM_MAKE = 'std::shared_ptr<Matter>'
 6    TO_MAKE = 'Matter*'
 7
 8    result = subprocess.run(['ag', '-l', FROM_MAKE],
 9                            capture_output=True, text=True)
10    files = result.stdout.splitlines()
11    actionable_files = [x for x in files if 'migrator' not in x]
12
13    for f in actionable_files:
14        file_path = Path(f)
15        if file_path.exists():
16            subprocess.run(['sd', FROM_MAKE, TO_MAKE,
17                            str(file_path)], check=True)
18        else:
19            print(f"File not found: {file_path}")
20
21replace_make_shared()

With sh, shell calls become more ergonomic, however, it is still easier to interoperate between the shell outputs and Python code (e.g. using echo) using xonsh.

Conclusions

Personally, xonsh hits the sweet-spot of being slightly less finicky than POSIX shells while being less verbose than the pure python variant. It isn’t perfect though:

  • The python dependence, even with pipx can be an issue 2
    • Packages which are needed have to be injected into the same environment..
      • nix maybe…
  • There is no good testing framework
    • Ugly subprocess based pytest tests could be written

For now, though, this is sufficiently advantageous.


  1. Like those mentioned in this list ↩︎

  2. True of all python packages ↩︎