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):
nThe nth argument.
And :t refers to the tail of that word:
tRemove all leading pathname components, leaving the tail. This works likebasename.
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:
rRemove 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 offoo.orig.cis.c, anddir.c/foohas 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).