How to make zsh vi-style commands respect WORDCHARS? - command-line

I prefer vi-style word movements such as vi-forward-word instead of forward-word, so that the cursor also stops on separator characters. However, I would also like to make zsh respect WORDCHARS in this case or somehow define my separator characters. Is this possible somehow? It seems I can either make zsh use my separator characters, or use vi-style movements where it also stops on them, but not both.
So e.g. if my line is the following:
% ls -la /foo/bar/f-b/r
then if I start moving forward word by word, it will stop on /, foo, /, bar, /, f-b, /, r.
Relevant lines from my .zshrc:
WORDCHARS='*?_-.[]~=&;!#$%^(){}<>' # removed /
autoload select-word-style
select-word-style normal
bindkey '^W' vi-backward-kill-word
bindkey '^f' vi-forward-word
bindkey '^b' vi-backward-word

In the end I just implemented my own zle widgets:
SEPCHARS='[/ ]'
my-forward-word() {
if [[ "${BUFFER[CURSOR + 1]}" =~ "${SEPCHARS}" ]]; then
(( CURSOR += 1 ))
return
fi
while [[ CURSOR -lt "${#BUFFER}" && ! "${BUFFER[CURSOR + 1]}" =~ "${SEPCHARS}" ]]; do
(( CURSOR += 1 ))
done
}
zle -N my-forward-word
bindkey '^f' my-forward-word
my-backward-word() {
if [[ "${BUFFER[CURSOR]}" =~ "${SEPCHARS}" ]]; then
(( CURSOR -= 1 ))
return
fi
while [[ CURSOR -gt 0 && ! "${BUFFER[CURSOR]}" =~ "${SEPCHARS}" ]]; do
(( CURSOR -= 1 ))
done
}
zle -N my-backward-word
bindkey '^b' my-backward-word
my-backward-kill-word() {
if [[ "${LBUFFER[CURSOR]}" =~ "${SEPCHARS}" ]]; then
LBUFFER="${LBUFFER[1, CURSOR - 1]}"
return
fi
while [[ CURSOR -gt 0 && ! "${LBUFFER[CURSOR]}" =~ "${SEPCHARS}" ]]; do
LBUFFER="${LBUFFER[1, CURSOR - 1]}"
done
}
zle -N my-backward-kill-word
bindkey '^W' my-backward-kill-word

Related

can't use '~' in zsh autocompletion

I use zsh and I want to use a function I wrote to replace cd.
This function gives you the ability to move to a parent directory:
$ pwd
/a/b/c/d
$ cl b
$ pwd
/a/b
You can also move into a subdirectory of a parent directory:
$ pwd
/a/b/c/d
$ cl b/e
$ pwd
/a/b/e
If the first part of the path is not a parent directory, it will just function as normal cd would. I hope that makes sense.
In summary, when in /a/b/c/d, I want to be able to move to /a, /a/b, /a/b/c, all subdirectories of /a/b/c/d and any absolute path starting with /, ~/ or ../ (or ./).
I hope that makes sense.
This is the function I wrote:
cl () {
local first=$( echo $1 | cut -d/ -f1 )
if [ $# -eq 0 ]; then
# cl without any arguments moves back to the previous directory
cd - > /dev/null
elif [ -d $first ]; then
# If the first argument is an existing normal directory, move there
cd $1
else
# Otherwise, move to a parent directory
cd ${PWD%/$first/*}/$1
fi
}
There is probably a better way to this (tips are welcome), but I haven't had any problems with this so far.
Now I want to add autocompletion. This is what I have so far:
_cl() {
pth=${words[2]}
opts=""
new=${pth##*/}
[[ "$pth" != *"/"*"/"* ]] && middle="" || middle="${${pth%/*}#*/}/"
if [[ "$pth" != *"/"* ]]; then
# If this is the start of the path
# In this case we should also show the parent directories
opts+=" "
first=""
d="${${PWD#/}%/*}/"
opts+="${d//\/// }"
dir=$PWD
else
first=${pth%%/*}
if [[ "$first" == "" ]]; then
# path starts with "/"
dir="/$middle"
elif [[ "$first" == "~" ]]; then
# path starts with "~/"
dir="$HOME/$middle"
elif [ -d $first ]; then
# path starts with a directory in the current directory
dir="$PWD/$first/$middle"
else
# path starts with parent directory
dir=${PWD%/$first/*}/$first/$middle
fi
first=$first/
fi
# List al sub directories of the $dir directory
if [ -d "$dir" ]; then
for d in $(ls -a $dir); do
if [ -d $dir/$d ] && [[ "$d" != "." ]] && [[ "$d" != ".." ]]; then
opts+="$first$middle$d/ "
fi
done
fi
_multi_parts / "(${opts})"
return 0
}
compdef _cl cl
Again, probably not the best way to do this, but it works... kinda.
One of the problems is that what I type cl ~/, it replaces it with cl ~/ and does not suggest any directories in my home folder. Is there a way to get this to work?
EDIT
cl () {
local first=$( echo $1 | cut -d/ -f1 )
if [ $# -eq 0 ]; then
# cl without any arguments moves back to the previous directory
local pwd_bu=$PWD
[[ $(dirs) == "~" ]] && return 1
while [[ $PWD == $pwd_bu ]]; do
popd >/dev/null
done
local pwd_nw=$PWD
[[ $(dirs) != "~" ]] && popd >/dev/null
pushd $pwd_bu >/dev/null
pushd $pwd_nw >/dev/null
elif [ -d $first ]; then
pushd $1 >/dev/null # If the first argument is an existing normal directory, move there
else
pushd ${PWD%/$first/*}/$1 >/dev/null # Otherwise, move to a parent directory or a child of that parent directory
fi
}
_cl() {
_cd
pth=${words[2]}
opts=""
new=${pth##*/}
local expl
# Generate the visual formatting and store it in `$expl`
_description -V ancestor-directories expl 'ancestor directories'
[[ "$pth" != *"/"*"/"* ]] && middle="" || middle="${${pth%/*}#*/}/"
if [[ "$pth" != *"/"* ]]; then
# If this is the start of the path
# In this case we should also show the parent directories
local ancestor=$PWD:h
while (( $#ancestor > 1 )); do
# -f: Treat this as a file (incl. dirs), so you get proper highlighting.
# -Q: Don't quote (escape) any of the characters.
# -W: Specify the parent of the dir we're adding.
# ${ancestor:h}: The parent ("head") of $ancestor.
# ${ancestor:t}: The short name ("tail") of $ancestor.
compadd "$expl[#]" -fQ -W "${ancestor:h}/" - "${ancestor:t}"
# Move on to the next parent.
ancestor=$ancestor:h
done
else
# $first is the first part of the path the user typed in.
# it it is part of the current direoctory, we know the user is trying to go back to a directory
first=${pth%%/*}
# $middle is the rest of the provided path
if [ ! -d $first ]; then
# path starts with parent directory
dir=${PWD%/$first/*}/$first
first=$first/
# List all sub directories of the $dir/$middle directory
if [ -d "$dir/$middle" ]; then
for d in $(ls -a $dir/$middle); do
if [ -d $dir/$middle/$d ] && [[ "$d" != "." ]] && [[ "$d" != ".." ]]; then
compadd "$expl[#]" -fQ -W $dir/ - $first$middle$d
fi
done
fi
fi
fi
}
compdef _cl cl
This is as far as I got on my own. It does works (kinda) but has a couple of problems:
When going back to a parent directory, completion mostly works. But when you go to a child of the paretn directory, the suggestions are wrong (they display the full path you have typed, not just the child directory). The result does work
I use syntax-hightlighting, but the path I type is just white (when using going to a parent directory. the normal cd functions are colored)
In my zshrc, I have the line:
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' '+l:|=* r:|=*'
Whith cd this means I can type "load" and it will complete to "Downloads". With cl, this does not work. Not event when using the normal cd functionality.
Is there a way to fix (some of these) problems?
I hope you guys understand my questions. I find it hard to explain the problem.
Thanks for your help!
This should do it:
_cl() {
# Store the number of matches generated so far.
local -i nmatches=$compstate[nmatches]
# Call the built-in completion for `cd`. No need to reinvent the wheel.
_cd
# ${PWD:h}: The parent ("head") of the present working dir.
local ancestor=$PWD:h expl
# Generate the visual formatting and store it in `$expl`
# -V: Don't sort these items; show them in the order we add them.
_description -V ancestor-directories expl 'ancestor directories'
while (( $#ancestor > 1 )); do
# -f: Treat this as a file (incl. dirs), so you get proper highlighting.
# -W: Specify the parent of the dir we're adding.
# ${ancestor:h}: The parent ("head") of $ancestor.
# ${ancestor:t}: The short name ("tail") of $ancestor.
compadd "$expl[#]" -f -W ${ancestor:h}/ - $ancestor:t
# Move on to the next parent.
ancestor=$ancestor:h
done
# Return true if we've added any matches.
(( compstate[nmatches] > nmatches ))
}
# Define the function above as generating completions for `cl`.
compdef _cl cl
# Alternatively, instead of the line above:
# 1. Create a file `_cl` inside a dir that's in your `$fpath`.
# 2. Paste the _contents_ of the function `_cl` into this file.
# 3. Add `#compdef cl` add the top of the file.
# `_cl` will now get loaded automatically when you run `compinit`.
Also, I would rewrite your cl function like this, so it no longer depends on cut or other external commands:
cl() {
if (( $# == 0 )); then
# `cl` without any arguments moves back to the previous directory.
cd -
elif [[ -d $1 || -d $PWD/$1 ]]; then
# If the argument is an existing absolute path or direct child, move there.
cd $1
else
# Get the longest prefix that ends with the argument.
local ancestor=${(M)${PWD:h}##*$1}
if [[ -d $ancestor ]]; then
# Move there, if it's an existing dir.
cd $ancestor
else
# Otherwise, print to stderr and return false.
print -u2 "$0: no such ancestor '$1'"
return 1
fi
fi
}
Alternative Solution
There is an easier way to do all of this, without the need to write a cd replacement or any completion code:
cdpath() {
# `$PWD` is always equal to the present working directory.
local dir=$PWD
# In addition to searching all children of `$PWD`, `cd` will also search all
# children of all of the dirs in the array `$cdpath`.
cdpath=()
# Add all ancestors of `$PWD` to `$cdpath`.
while (( $#dir > 1 )); do
# `:h` is the direct parent.
dir=$dir:h
cdpath+=( $dir )
done
}
# Run the function above whenever we change directory.
add-zsh-hook chpwd cdpath
Zsh's completion code for cd automatically takes $cdpath into account. No need to even configure that. :)
As an example of how this works, let's say you're in /Users/marlon/.zsh/prezto/modules/history-substring-search/external/.
You can now type cd pre and press Tab, and Zsh will complete it to cd prezto. After that, pressing Enter will take you directly to /Users/marlon/.zsh/prezto/.
Or let's say that there also exists /Users/marlon/.zsh/prezto/modules/prompt/external/agnoster/. When you're in the former dir, you can do cd prompt/external/agnoster to go directly to the latter, and Zsh will complete this path for you every step of the way.

posix sh: how to count number of occurrences in a string without using external tools?

In bash, it can be done like this:
#!/bin/bash
query='bengal'
string_to_search='bengal,toyger,bengal,persian,bengal'
delimiter='|'
replace_queries="${string_to_search//"$query"/"$delimiter"}"
delimiter_count="${replace_queries//[^"$delimiter"]}"
delimiter_count="${#delimiter_count}"
echo "Found $delimiter_count occurences of \"$query\""
Output:
Found 3 occurences of "bengal"
The caveat of course is that the delimiter cannot occur in 'query' or 'string_to_search'.
In POSIX sh, string replacement is not supported. Is there a way this can be done in POSIX sh using only shell builtins?
#!/bin/sh
query='bengal'
string_to_search='bengal,toyger,bengal,persian,bengal'
ct() (
n=0
IFS=,
q=$1
set $2
for t in "$#"; do
if [ "$t" = "$q" ]; then
n=$((n + 1))
fi
done
echo $n
)
n=$(ct "$query" "$string_to_search")
printf "found %d %s\n" $n $query
Though I'm not sure what the point is. If you've got a posix shell,
you also almost certainly have printf, sed, grep, and wc.
printf '%s\n' "$string_to_search" | sed -e 's/,/\n/g' | grep -Fx "$query" | wc -l
Think I got it...
#!/bin/sh
query='bengal'
string_to_search='bengal,toyger,bengal,persian,bengal'
i=0
process_string="$string_to_search"
while [ -n "$process_string" ]; do
case "$process_string" in
*"$query"*)
process_string="${process_string#*"$query"}"
i="$(( i + 1 ))"
;;
*)
break
;;
esac
done
echo "Found $i occurences of \"$query\""

How to remove YAML frontmatter from markdown files?

I have markdown files that contain YAML frontmatter metadata, like this:
---
title: Something Somethingelse
author: Somebody Sometheson
---
But the YAML is of varying widths. Can I use a Posix command like sed to remove that frontmatter when it's at the beginning of a file? Something that just removes everything between --- and ---, inclusive, but also ignores the rest of the file, in case there are ---s elsewhere.
I understand your question to mean that you want to remove the first ----enclosed block if it starts at the first line. In that case,
sed '1 { /^---/ { :a N; /\n---/! ba; d} }' filename
This is:
1 { # in the first line
/^---/ { # if it starts with ---
:a # jump label for looping
N # fetch the next line, append to pattern space
/\n---/! ba; # if the result does not contain \n--- (that is, if the last
# fetched line does not begin with ---), go back to :a
d # then delete the whole thing.
}
}
# otherwise drop off the end here and do the default (print
# the line)
Depending on how you want to handle lines that begin with ---abc or so, you may have to change the patterns a little (perhaps add $ at the end to only match when the whole line is ---). I'm a bit unclear on your precise requirements there.
If you want to remove only the front matter, you could simply run:
sed '1{/^---$/!q;};1,/^---$/d' infile
If the first line doesn't match ---, sed will quit; else it will delete everything from the 1st line up to (and including) the next line matching --- (i.e. the entire front matter).
If you don't mind the "or something" being perl.
Simply print after two instances of "---" have been found:
perl -ne 'if ($i > 1) { print } else { /^---/ && $i++ }' yaml
or a bit shorter if you don't mind abusing ?: for flow control:
perl -ne '$i > 1 ? print : /^---/ && $i++' yaml
Be sure to include -i if you want to replace inline.
you use a bash file, create script.sh and make it executable using chmod +x script.sh and run it ./script.sh.
#!/bin/bash
#folder articles contains a lot of markdown files
files=./articles/*.md
for f in $files;
do
#filename
echo "${f##*/}"
#replace frontmatter title attribute to "title"
sed -i -r 's/^title: (.*)$/title: "\1"/' $f
#...
done
This AWK based solution works for files with and without FrontMatter, doing nothing in the later case.
#!/bin/sh
# Strips YAML FrontMattter from a file (usually Markdown).
# Exit immediately on each error and unset variable;
# see: https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -Ee
print_help() {
echo "Strips YAML FrontMattter from a file (usually Markdown)."
echo
echo "Usage:"
echo " `basename $0` -h"
echo " `basename $0` --help"
echo " `basename $0` -i <file-with-front-matter>"
echo " `basename $0` --in-place <file-with-front-matter>"
echo " `basename $0` <file-with-front-matter> <file-to-be-without-front-matter>"
}
replace=false
in_file="-"
out_file="/dev/stdout"
if [ -n "$1" ]
then
if [ "$1" = "-h" ] || [ "$1" = "--help" ]
then
print_help
exit 0
elif [ "$1" = "-i" ] || [ "$1" = "--in-place" ]
then
replace=true
in_file="$2"
out_file="$in_file"
else
in_file="$1"
if [ -n "$2" ]
then
out_file="$2"
fi
fi
fi
tmp_out_file="$out_file"
if $replace
then
tmp_out_file="${in_file}_tmp"
fi
awk -e '
BEGIN {
is_first_line=1;
in_fm=0;
}
/^---$/ {
if (is_first_line) {
in_fm=1;
}
}
{
if (! in_fm) {
print $0;
}
}
/^(---|...)$/ {
if (! is_first_line) {
in_fm=0;
}
is_first_line=0;
}
' "$in_file" >> "$tmp_out_file"
if $replace
then
mv "$tmp_out_file" "$out_file"
fi

Using CVS diff to get line numbers of mofied/added/deleted code

I have a very large project where we have made changes to lots of files. Now we need to exactly know which line numbers we have changed/added so that we can run some other tool on that particular line only.
While searching for the answer i found this as an answer to some other question..
echo ${f}:
for n in $(git --no-pager blame --line-porcelain $1 |
awk '/author Not Committed Yet/{if (a && a !~ /author Not Committed Yet/) print a} {a=$0}' |
awk '{print $3}') ; do
if (( prev_line > -1 )) ; then
if (( "$n" > (prev_line + 1) )) ; then
if (( (prev_line - range_start) > 1 )) ; then
echo -n "$range_start-$prev_line,"
else
echo -n "$range_start,$prev_line,"
fi
range_start=$n
fi
else
range_start=$n
fi
prev_line=$n
done
if (( "$range_start" != "$prev_line" )) ; then
echo "$range_start-$prev_line"
else
echo "$range_start"
fi
And it ends up looking like this:
views.py:
403,404,533-538,546-548,550-552,554-559,565-567,580-582
It does great.. i need the exact same output using cvs.. Is there any way to get similar output..

How to delete multiple empty lines with SED?

I'm trying to compress a text document by deleting of duplicated empty lines, with sed. This is what I'm doing (to no avail):
sed -i -E 's/\n{3,}/\n/g' file.txt
I understand that it's not correct, according to this manual, but I can't figure out how to do it correctly. Thanks.
I think you want to replace spans of multiple blank lines with a single blank line, even though your example replaces multiple runs of \n with a single \n instead of \n\n. With that in mind, here are two solutions:
sed '/^$/{ :l
N; s/^\n$//; t l
p; d; }' input
In many implementations of sed, that can be all on one line, with the embedded newlines replaced by ;.
awk 't || !/^$/; { t = !/^$/ }'
As tripleee suggested above, I'm using Perl instead of sed:
perl -0777pi -e 's/\n{3,}/\n\n/g'
Use the translate function
tr -s '\n'
the -s or --squeeze-repeats reduces a sequence of repeated character to a single instance.
This is much better handled by tr -s '\n' or cat -s, but if you insist on sed, here's an example from section 4.17 of the GNU sed manual:
#!/usr/bin/sed -f
# on empty lines, join with next
# Note there is a star in the regexp
:x
/^\n*$/ {
N
bx
}
# now, squeeze all '\n', this can be also done by:
# s/^\(\n\)*/\1/
s/\n*/\
/
I am not sure this is what the OP wanted but using the awk solution by William Pursell here is the approach if you want to delete ALL empty lines in the file:
awk '!/^$/' file.txt
Explanation:
The awk pattern
'!/^$/'
is testing whether the current line is consisting only of the beginning of a line (symbolised by '^') and the end of a line (symbolised by '$'), in other words, whether the line is empty.
If this pattern is true awk applies its default and prints the current line.
HTH
I think OP wants to compress empty lines, e.g. where there are 9 consecutive emty lines, he wants to have just three.
I have written a little bash script that does just that:
#! /bin/bash
TOTALLINES="$(cat file.txt|wc -l)"
CURRENTLINE=1
while [ $CURRENTLINE -le $TOTALLINES ]
do
L1=$CURRENTLINE
L2=$(($L1 + 1))
L3=$(($L1 +2))
if [[ $(cat file.txt|head -$L1|tail +$L1) == "" ]]||[[ $(cat file.txt|head -$L1|tail +$L1) == " " ]]
then
L1EMPTY=true
else
L1EMPTY=false
fi
if [[ $(cat file.txt|head -$L2|tail +$L2) == "" ]]||[[ $(cat file.txt|head -$L2|tail +$L2) == " " ]]
then
L2EMPTY=true
else
L2EMPTY=false
fi
if [[ $(cat file.txt|head -$L3|tail +$L3) == "" ]]||[[ $(cat file.txt|head -$L3|tail +$L3) == " " ]]
then
L3EMPTY=true
else
L3EMPTY=false
fi
if [ $L1EMPTY = true ]&&[ $L2EMPTY = true ]&&[ $L3EMPTY = true ]
then
#do not cat line to temp file
echo "Skipping line "$CURRENTLINE
else
echo "$(cat file.txt|head -$CURRENTLINE|tail +$CURRENTLINE)">>temp.txt
echo "Writing line " $CURRENTLINE
fi
((CURRENTLINE++))
done
cat temp.txt>file.txt
rm -r temp.txt
FINALTOTALLINES="$(cat file.txt|wc -l)"
EMPTYLINELINT=$(( $CURRENTLINE - $FINALTOTALLINES ))
echo "Deleted " $EMPTYLINELINT " empty lines."