1

I want to grep a string from a given character or pattern until another given character or pattern instead of the entire line.

For example:

$ > echo "The brown fox jumps over the lazy dog" | grep -option "b" "s"
brown fox jumps

$ > echo "The brown fox jumps over the lazy dog" | grep -option "the l" "g"
the lazy dog

What I actually need it for is to grep a path when I use dirs -v so that I can do this:

$ > ls -AF
file.txt   github/    .vim/

$ > dirs -v
0    ~
1    ~/.vim/pack/themes
2    ~/.vim/colors
3    ~/github/dirname
4    ~/github/repo/

$ > mv -v file.txt $(dirs -v | grep 2 | grep -option "~" "colors")
renamed 'file.txt' -> '~/.vim/colors/file.txt'

I used grep twice so that I can only match the ~ with the one from the line containing 2. Is there a way to accomplish this as a shell function/alias in my .zshrc?


ANSWER: I simply used mv file.txt ~2 from one of the answers below. I didn't even think I could do that, lol.

I also put these lines in my .zshrc from one of the answers below.

function grepo () { grep -Po "${1}.*?${2}" }
alias -g GO='| grepo'

so that I can use it like so:

$ > echo "the brown fox jumps over the lazy dog" GO b s
brown fox jumps

$ > echo "the brown fox jumps over the lazy dog" GO "the l" g
the lazy dog

$ > echo "the brown fox jumps over the lazy dog" GO fox over
fox jumps over

For my problem, it didn't work though for some reason I can't think of.

$ > ls -AF
file.txt   github/    .vim/

$ > dirs -v
0    ~
1    ~/.vim/pack/themes
2    ~/.vim/colors
3    ~/github/dirname
4    ~/github/repo/

$ > mv file.txt $(dirs -v GO "~/.v" "themes")
mv: cannot move 'file.txt' to '~/.vim/pack/themes': No such file or directory
ntruter42
  • 187
  • 1
    You may want to make it grepo () grep -Poe "$1.*?$2" (to avoid problems when $1 starts with -, or evan grepo () grep -Po "\Q$1\E.*?\Q$2\E", so $1 and $2 are not interpreted as regexps (like your ~/.v case where . otherwise matches any single character). – Stéphane Chazelas May 15 '20 at 12:46

3 Answers3

3

Use a lazy match (.*?) from PCRE regex:

$ str='The brown fox jumps over the lazy dog'

$ grep -Po 'b.*?s' <<<"$str"
brown fox jumps

$ grep -Po 'the l.*?g' <<<"$str"
the lazy dog

There is a similar solution for more basic regex, but works only for single characters (on the Right Hand):

$ grep -o 'b[^s]*s' <<<"$str"
brown fox jumps

$ grep -o 'the l[^g]*g' <<<"$str"
the lazy dog

All the above will match the first initial text and then until the first final text (included).

3

In zsh, you can do:

$ str='The brown fox jumps over the lazy dog'
$ print -r -- ${(SM)str#b*s}
brown fox jumps

The ${str#pattern} ksh operator removes the shortest string that matches the pattern from the start of the string. The S (for substring) parameter expansion flag extends it so that the pattern is not restricted to match at the start, but as close as possible from the start. The M flag (for matched) makes those expansion operators expand to what is matched instead of removing the matched portion.

Using ## instead of # gets you the longest instead of shortest and %/%% look for the pattern from the end of the string.

You can also do PCRE matching and use its *? non-greedy operator with:

$ zmodload zsh/pcre
$ [[ $str -pcre-match 'b.*?s' ]] && print -r -- $MATCH
brown fox jumps

or:

$ set -o rematchpcre
$ [[ $str =~ 'b.*?s' ]] && print -r -- $MATCH
brown fox jumps

For your specific use case though, I'd just do:

mv file.txt ~2

To move the file to the second directory in your dirstack.

If configured properly, the completion would show you what those ~<number> correspond to after you press Tab after ~+:

$ mv file ~+Tab
Completing directory stack
1 -- ~/install/cvs/zsh/Config
2 -- ~
3 -- /usr
4 -- ~bin
5 -- ~
6 -- /

A minimum ~/.zshrc to produce that output would look like:

set -o autopushd
autoload -Uz compinit
compinit
zstyle ':completion:*' menu select=0
zstyle ':completion:*' verbose true
zstyle ':completion:*' format 'Completing %d'

(see also compinstall to customize completion in a more beginner-friendly way).

2

There's an easier and more reliable way to do this.

In zsh, use the dirstack variable. No quoting required because zsh doesn't do anything fancy with variable expansion¹.

mv file.txt $dirstack[2]

If you want a parent directory, you can append the history modifier :h. For example, to move the file to the parent of the directory stack entry, use mv file.txt $dirstack[2]:h. To move to the parent of the parent, use mv file.txt $dirstack[2]:h:h or mv file.txt $dirstack[2]:h2.

You can also use parameter expansion constructs to modify the directory name, but it gets more complicated.

Alternatively, to modify the target directory, press Tab after $dirstack[2] and zsh will replace the variable expansion by its value¹.

In bash, use the DIRSTACK variable. Quotes and braces are required.

mv file.txt "${DIRSTACK[2]}"

You can use one parameter expansion construct on the resulting value. For example, to move the file to the parent directory of the directory stack entry, use mv file.txt "${DIRSTACK[2]#/*}".


$ > mv -v file.txt $(dirs -v | grep 2 | grep -option "~" "colors")

This is fragile. What if a directory name contains a 2? Or if there are more than 12 directories on the stack? And it won't even work, because the tilde is not expanded in the result of the command substitution. You'd need to use something like

"$(HOME=/none dirs -v | grep '^ *2[^0-9]' | sed 's:^[^/]*::')"
  • Double quotes around the command substitution in case the resulting directory name contains whitespace.
  • Match the 2 only at the beginning of the line (allowing leading spaces).
  • Don't match 2 if it's the leading digit of a multiple-digit number.
  • Use sed to remove everything up to the start of the directory name. Since HOME was set to a nonexistent path while running dirs, the directory name is guaranteed to start with a slash.
  • You'd only need an extra grep if you want to pick only part of the directory name.

Awk might be a better tool for this, but to some extent whether to use a single more generic tool (awk) or combine more specialized tools (grep, sed, …) is a matter of taste.

"$(HOME=/none dirs -v | awk '$1 == 2 {sub(/^[ \t0-9]+/, ""); print; exit}')"

¹ Depending on your completion and key binding settings, you may need another key. Discussing the possibilities is beyond the scope of this answer.
² Except removing empty words but that doesn't matter here as long as you make sure that the directory stack is deep enough.
³ You don't need the quotes if the directory name doesn't contain whitespace (or wildcard characters).