3

I often clone a git repository then enter its root directory. For example:

$ git clone https://github.com/hpjansson/chafa && cd chafa

To make this a little easier, I have a zsh abbreviation – cc – which is expanded into && cd !#:2:t:

typeset -Ag abbrev
abbrev=(
  'G' '| grep -v grep | grep'
  'L' '2>&1 | less'
  'V' '2>&1 | vipe >/dev/null'
  'ac' '| column -t'
  'bl' '; tput bel'
  'cc' '&& cd !#:2:t'
  'fl' "| awk '{ print $"
  'ne' '2>/dev/null'
  'pf' "printf -- '"
  'sl' '>/dev/null 2>&1'
  'tl' '| tail -20'
)
__abbrev_expand() {
  emulate -L zsh
  setopt EXTENDED_GLOB
  local MATCH
  LBUFFER=${LBUFFER%%(#m)[a-zA-Z]#}
  if [[ "${LBUFFER: -1}" == ' ' ]]; then
    LBUFFER+=${abbrev[$MATCH]:-$MATCH}
    if [[ $MATCH = 'fl' ]]; then
      RBUFFER="}'"
    elif [[ $MATCH = 'pf' ]]; then
      RBUFFER="\n'"
    fi
  else
    LBUFFER+=$MATCH
  fi
  zle self-insert
}
zle -N __abbrev_expand
bindkey ' ' __abbrev_expand
bindkey -M isearch ' ' self-insert

!# refers to the current command-line, as described in man zshexpn (section HISTORY EXPANSION, subsection Event Designators):

!# Refer to the current command line typed in so far. The line is treated as if it were complete up to and including the word before the one with the !# reference.

:2 refers to the second word on the command-line (which is the url when I type a $ git clone command):

n The nth argument.

And :t refers to the tail of that word:

t Remove all leading pathname components, leaving the tail. This works like basename.


Typically, I will type git clone on the command-line, then copy-paste the url of the project, then insert a space, cc and another space, which gives the desired command.

However, sometimes, the url that I copy-paste ends with the extension .git. When that happens the expansion of !#:2:t contains an undesirable .git extension:

$ git clone https://github.com/hpjansson/chafa.git cc
→
$ git clone https://github.com/hpjansson/chafa.git && cd !#:2:t
→
$ git clone https://github.com/hpjansson/chafa.git && cd chafa.git
cd: no such file or directory: chafa.git

One solution is to also use the :r modifier to remove the .git extension:

r Remove a filename extension leaving the root name. Strings with no filename extension are not altered. A filename extension is a . followed by any number of characters (including zero) that are neither . nor / and that continue to the end of the string. For example, the extension of foo.orig.c is .c, and dir.c/foo has no extension.

                                                               vv
$ git clone https://github.com/hpjansson/chafa.git && cd !#:2:t:r
→
$ git clone https://github.com/hpjansson/chafa.git && cd chafa

However, if the path does not end with a .git extension, then the expansion fails and neither the git command nor the cd command is executed.

Here is a minimal example to illustrate the issue:

$ echo /foo/bar.baz !#:1:t:r
/foo/bar.baz bar

$ echo /foo/bar !#:1:t:r
zsh: modifier failed: r

The first command succeeds, because the last path component contains the extension .baz, but the second one fails because there is no extension anymore. OTOH, in bash 4.3.48, both commands work as expected.

I don't understand why :r fails when there is no extension, because its documentation contains this sentence:

Strings with no filename extension are not altered.

It doesn't say that using :r for a string which doesn't contain an extension is an error.


I tried another approach; removing the extension with the :s modifier. I think this requires the HIST_SUBST_PATTERN option to be set:

setopt HIST_SUBST_PATTERN

With this option, one can write :s/.* to remove an extension:

                          vvvvv
$ echo /foo/bar.baz !#:1:t:s/.*
/foo/bar.baz bar

It works when there is an extension, but again fails when there is none:

$ setopt HIST_SUBST_PATTERN
$ echo /foo/bar !#:1:t:s/.*
zsh: substitution failed

Is there a single sequence of modifiers which can simultaneously refer to bar in /foo/bar.baz, and to bar in /foo/bar?

I'm using zsh 5.7.1-dev-0 (x86_64-pc-linux-gnu).

3

I can't think of a way to do this with history expansion. But I don't think history expansion is the best tool here. It's very limited. From a zle widget, you can access the command line and manipulate it with arbitrary code. Take advantage of this.

One approach would be to do expansion on the abbreviation: instead of $abbrev[$MATCH], use ${(e)abbrev[$MATCH]}. Obviously you'll need to change your abbreviations to quote special characters appropriately. For cc, use something like

&& cd ${${(z)BUFFER}[3]}

For the specific case of git clone, and more generally for anything that creates a directory in the current directory, you could use a different abbreviation: switch to the newly created directory. That works with git clone https://example.com/foo.git, for git clone https://example.com/foo, and for git clone https://example.com/foo.git bar, as well as for tar xf foo.tar (if the archive has a single toplevel directory) and mv /some/directory ..

&& cd *(/oc[1])

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.