Shell – shell which can rapidly allow reordering arguments

shellzsh

I use the command line heavily and over the years have migrated from bash to zsh as a daily driver shell. I usually use a slightly customized oh-my-zsh environment, but some systems are on prezto; the differences are not large.

The most productive plugins I've been using for zsh are zsh-syntax-highlighting and history-substring-search, and lately I've been using the very powerful fzf plugins for pulling up history.

Now, I'm finding one of the biggest pain points that remain for me in the command line is command argument reordering. Quite often I try to run a command

command very/long/filesystem/path/to/argumentA another/filesystem/or/network/path/argumentB

and realize I've got the order backwards.

Another even more common situation is when we do any "manual deployment" workflow: First you compare the new stuff with the real stuff, e.g.

diff /opt/app/static/www/a.html /home/user/docs/dev/src/a.html
cp /home/user/docs/dev/src/a.html /opt/app/static/www/a.html

Ok, ok, last example (this one has several steps), no more I promise. Perfect real world example right here. Let's get cracking with some file listing with sweet human sizes:

find /pool/backups -type f -print0 | xargs -0 ls -lh > filelisting

I want to size sort and pick some out interactively:

sort -rhk5 filelisting | fzf -m > /these/are/the_chosen

Nice, that works! Oh but I need just the paths now, but don't want to re-run find:

cut -d ' ' -f 10 /these/are/the_chosen

The output is garbage, because we encountered a setback with ls -lh getting frisky with spaces. But I've got a strategy: Join the contiguous spaces. Let's opt for tr -s to squeeze space chars, no need for a regex here. Though, tr requires stdin:

cat /these/are/the_chosen | tr -s ' ' | cut -d ' ' -f 9- > filelist

By this point, we're feeling the pain of cutting and pasting arguments around in commands. My choices here are always between awkward alternatives: I can move the long path or i can move the commands. With either move, I have to either reach over for the mouse to copy it, or i have to type it again in the new spot. I can't win. With a command line, even navigating around is cumbersome, and extremely so without word hopping hotkeys set up.

I can't even use my mouse to jump around rapidly on a shell prompt! (Hey, does anyone know of a shell that supports mouse events?) So, this is the inefficiency that I want to abolish. I want to eliminate the friction of grabbing a shell object (such as a valid path string) and move it freely left and right while I prototype out monster pipelines. If I had that feature I could spam that 5 times to shove the path to the left of the cut & flags, then construct the rest of the pipeline organically.

I believe the lack of line editing power is what the issue is here. In the very first example where I want to transpose the first 2 args, I can create a trivial shell script that perhaps I'd call cpto that inverts the arguments and delegates to the cp command. But I don't want to have to do that, and it would not help me in the general case, like in the third example.

I'd like to be able to reorder the arguments that I've entered using a simple key combination, like I can do for various types of lists if I'm in Vim with plugins like sideways.

Does such a plugin exist for zsh? Does such a plugin exist for any other shells?

If not, how difficult would it be to implement for zsh? I think that the zsh-syntax-highlighting plugin proves that it should be possible to tokenize arguments. Indeed the shell knows how to fetch individual arguments from history: https://stackoverflow.com/a/21439621/340947

The pain point is so severe and common that I'm liable to write a simple script to bind to a hotkey that grabs the last entry in history and swaps the last 2 args for me, and runs that. But that would not be as ideal as having a line editor operation so that the swap can be done interactively rather than committing to run the command.

Perhaps an improvement on that could be injecting !:0 !:2 !:1 (which zsh nicely auto expands for me) but there are plenty of problems with this also: (1) it won't work without already having attempted to run the wrong command. More than half the time I want to swap args after catching myself after having typed an incorrect command, and (2) often there are flags that were used which that snippet would fail to account for.

I've also seen the approach shown here which is fine but remains tremendously unsatisfying as the keystrokes need to be repeated a lot for long paths, and the Ctrl+Y behavior only recalls the most recent item that was cut, rather than hold a stack of them. It's good to know, but practically useless to me.

For completeness' sake, the tactic taken now is to use whatever suitable key combo to delete words to erase the shorter of the commands to reorder, reposition the cursor, use the mouse to copy the deleted argument from terminal output, and paste it back in. Ordinary folk don't bat an eye at this but it makes me die a little every time I do it because I cannot stop thinking about how easy it would be for the computer to do this task for me, and the injustice that I feel having to reach my hand over to the mouse.

Best Answer

In zsh, by default all the widgets that operate on words including the transpose-words one bound by default to Alt+T in emacs mode work on words that are defined as sequences of alnum+$WORDCHARS characters.

The default value of $WORDCHARS has *?_-.[]~=/&;!#$%^(){}<>, so includes /, so should be fine for you to transpose paths as long as those paths don't include characters outside of that. That won't work for paths that contain things like :, @, ,... or are quoted though.

But you could use the select-word-style framework to change the definition of word on-demand.

If you add:

autoload -U select-word-style
zle -N select-word-style
bindkey '\ez' select-word-style

to you ~/.zshrc, then upon pressing Alt+Z, you'll get the choice:

Word styles (hit return for more detail):
(b)ash (n)ormal (s)hell (w)hitespace (d)efault (q)uit
(B), (N), (S), (W) as above with subword matching
?

After pressing "return for more detail":

(b)ash:       Word characters are alphanumerics only
(n)ormal:     Word characters are alphanumerics plus $WORDCHARS
(s)hell:      Words are command arguments using shell syntax
(w)hitespace: Words are whitespace-delimited
(d)efault:    Use default, no special handling (usually same as `n')
(q)uit:       Quit without setting a new style

so pressing S would allow you to transpose two shell words (so including those containing quoted spaces or command substitutions...) with Alt+T (or delete one with Ctrl+W, move back one with Alt+B, etc).

See

info zsh select-word-style

for details (assuming the zsh documentation has been installed on your system (zsh-doc package on Debian and derivatives)).

You'll find a section there that looks like it has been especially written for you which you can adapt to specify how you want transpose-words to behave whenever the cursor is on a filename or in-between words, etc:

Here are some examples of use of the word-context style to extend the context.

   zstyle ':zle:*' word-context \
          "*/*" filename "[[:space:]]" whitespace
   zstyle ':zle:transpose-words:whitespace' word-style shell
   zstyle ':zle:transpose-words:filename' word-style normal
   zstyle ':zle:transpose-words:filename' word-chars ''

This provides two different ways of using transpose-words depending on whether the cursor is on whitespace between words or on a filename, here any word containing a /. On whitespace, complete arguments as defined by standard shell rules will be transposed. In a filename, only alphanumerics will be transposed. Elsewhere, words will be transposed using the default style for :zle:transpose-words.

For instance, with:

autoload -U select-word-style
zle -N select-word-style
bindkey '\ez' select-word-style
select-word-style normal
zstyle :zle:transpose-words word-style shell

transpose-words would work with shell words always while all other word widgets would use the normal definition of word, and you could still use Alt+Z to change it (for widgets other than transpose-words).

Related Question