📜 ⬆️ ⬇️

Zsh life tricks

Zsh is one of the best command shells, with an impressive array of features. However, due to the large number of possibilities, it is not surprising that some of them pass by the attention or the possibility of using them to solve everyday tasks is not obvious. This article will look at both a few “built-in” features of zsh, as well as examples of complex code that make life easier.

1. Using READNULLCMD


The variable READNULLCMD specifies the command to be called if the stdin redirection is used without entering the command: <file.txt . So you can call less by typing a lot less characters: just set READNULLCMD=less .

2. Insert the opening and closing brackets


Editors like Vim often use add-ons that automatically close parentheses when they are entered. Those. when you enter [ you get [] with a cursor in the middle. In shells, this is also possible (even in bash): you just need to use something like binkey -s "[" $'\Cv[]\C-b' : the equivalent of this command may well be placed in .inputrc. A more versatile solution for zsh is to use ZLE widgets:
 insert-double-brackets() { LBUFFER="${LBUFFER}[[ " RBUFFER=" ]]${RBUFFER}" } zle -N insert-double-brackets bindkey ',H' insert-double-brackets 
Here, the LBUFFER variable contains the entire command line up to the cursor, and the RBUFFER variable contains the whole after. The second command creates a widget, the third assigns it to the combination ,H : thus input ,H turns into [[ ]] with the cursor in the middle.

3. Global alias


You all probably know what an alias is in a shell, and you may have used something like alias hp='hg push' . Alias ​​in zsh have two additional features: suffix alias, which allow you to automatically open files without entering a program (example: alias -s txt=vim turns the foo.txt command into vim foo.txt ) and global ones. I have never used the first ones, but I find the second ones quite useful.
')
Global alias are used to replace single words with their meaning. Unlike suffix and ordinary alias, the replaced word does not have to be in the command position (i.e., the first word on the command line, or the first word after the command separator). Since alias are processed before the main parser works, you can easily have anything in the global alias: redirection, if , command delimiters.

From my point of view, redirection of various kinds is most useful:
 alias -g NN='&>/dev/null' alias -g L='|less' alias -g G='|grep' 
In this example, three alias are defined: one silences the command, another uses less to show the output of the command, the third filters input. Example use: write hg cat -r default file.csv G 42 L equivalent to hg cat -r default file.csv | grep 42 | less hg cat -r default file.csv | grep 42 | less hg cat -r default file.csv | grep 42 | less , but much shorter. To submit the G command to the input, it is literally necessary to use shielding: \G or 'G' . Note that \G and 'G' also form words, and they can also have alias: alias -g "'G'=|grep" , but I hope you are sane enough to not use this fact.

Despite its convenience, due to some features of zsh, global alias are very dangerous because they can spoil zsh add-ons. I saw in one case script, where there was also a condition of the form L) , and it did not work because of its transformation into a completely different condition. Therefore, global alias should be determined most recently, after you have already downloaded all the add-ons. To load add-ons after detection, disable the ALIASES setting: use something like
 source() { setopt localoptions setopt noaliases builtin source "${@[@]}" } .() { setopt localoptions setopt noaliases builtin . "${@[@]}" } 
And so for each option of loading add-ons (besides source and . There is also at least autoload , about the effectiveness of just such functions for which I’m not sure what I’ve done). Global alias, however, are dangerous only in an interactive session, scripts with #!/bin/zsh will not be affected.

4. Returning terminal settings


It is not a secret to anyone that if you write cat /bin/test (more precisely, cat any-binary-file ), you can get various strange effects: for example, replacing part of the characters entered further with symbols for drawing graphics. Most effects are eliminated by writing blindly echo $'\ec' , but this is the thing that I would like to automate. The hook precmd will help us in this, allowing you to run your function right before the shell is displayed. The problems that I sometimes see, if I accidentally output a binary file to the terminal, my editor (Vim) crashes, or I just run wine (for some reason it switches the keyboard transmit mode and does not return): graphic symbols instead of normal, alternate screen becomes main (= there is no scrollback (input history)), the arrows stop working (the keyboard transmit is marked here), the cursor is not displayed. To solve them, the following function was created:
 _echoti() { emulate -L zsh (( ${+terminfo[$1]} )) && echoti $1 } term_reset() { emulate -L zsh [[ -n $TTY ]] && (( $+terminfo )) && { _echoti rmacs #    _echoti sgr0 #   _echoti cnorm #   _echoti smkx #  «keyboard transmit mode» echo -n $'\e[?47l' #  alternate screen # See https://github.com/fish-shell/fish-shell/issues/2139 for smkx } } zmodload zsh/terminfo && precmd_functions+=( term_reset ) ttyctl -f 
. After its introduction to type echo $'\ec' I almost no longer have to.

Also note ttyctl -f : this built-in zsh feature blocks some changes to the terminal settings: those settings that are set using stty , and not those that can be set using special sequences (escape sequences).

5. zmv function


You may have come across the rename command to automatically rename multiple files. It even exists in two copies: a variant written in perl and written in C. Zsh has something similar, but only more powerful: first, you can copy files this way or run hg mv instead of just moving around the mv type. Secondly, you can use an “intuitive” version like noglob zmv -W *.c *.cpp (to get rid of noglob , use alias ; in the following examples, noglob implied). Zmv does not use regular expressions for work, but expressions more suitable for the glob task. You can also use virtually any expression as the second argument: zmv -w test_*.c 'test/${1/_foo/_bar}' will turn test_foo_1.c into test_bar_1.c . Here, parameters like $N provide access to the “capturing groups” analog from regular expressions, and -w turns test_*.c into test_(*).c

All arguments:

6. Run mpv with automatically found subtitles


If you have ever downloaded serials with external subtitles from torrents, then you have undoubtedly noticed that every person posting them has his own opinion as to where the subtitles should be. There are two main options: in your own directory and directly next to the video, but under the “own directory” any directory name can be hidden, and even different embedding depths: I saw directories like “subs {sub group}”, “subtitles {sub group}” , "Subs / {sub group}" and even simply "{sub group}". An additional problem is the use of non-standard fonts in subtitles, with their distribution along with subtitles.

You can use different methods to ensure that the subtitles are still picked up and use the correct fonts. I chose to create a function that automatically does the necessary work in almost all cases:
 aplayer() { emulate -L zsh setopt extendedglob setopt nullglob local -a args args=() local -A mediadirs mediadirs=() for arg in $@ ; do if [[ ${arg[0]} == '-' ]] ; then continue fi if test -f $arg ; then mediadirs[${arg:A:h}]=1 fi done local d local -i found=0 for d in ${(k)mediadirs} ; do local tail=$d:t test -d ~/.fonts/aplayer/${tail}-1 && continue local f for f in $d/**/(#i)font* ; do if test -d $f ; then (( found++ )) ln -s $f ~/.fonts/aplayer/${tail}-${found} elif [[ $f == (#i)*.rar ]] || [[ $f == (#i)*.zip ]] ; then (( found++ )) mkdir ~/.fonts/aplayer/${tail}-${found} pushd -q ~/.fonts/aplayer/${tail}-${found} 7z x $f popd -q fi done done if (( found )) ; then fc-cache -v ~/.fonts fi local -aT subpaths SUBPATHS local -A SUBPATHS_MAP SUBPATHS=( ${(k)^mediadirs}/(#i)*(sub|)*{,/**/*}(/) ) for sp in $SUBPATHS ; do SUBPATHS_MAP[$sp]=1 done local -a subarr for d in ${(k)mediadirs} ; do for subd in $d/**/ ; do if ! test -z $SUBPATHS_MAP[$d] ; then continue fi subarr=( $subd/*.(ass|ssa|srt) ) if (( $#subarr )) ; then SUBPATHS_MAP[$subd]=1 SUBPATHS+=( $subd ) fi done done if (( ${#SUBPATHS} )) ; then args+=( --sub-paths $subpaths ) fi mpv $args $@ &>/dev/tty } 
Having zsh things like associative arrays helps a lot when creating such functions.

Here, the first part of the function is traversed by all arguments and clogs the catalogs in which the works are located in the associative array mediadirs. It is made associative solely to avoid duplicates.

Further, the function is traversed in all directories with works and finds fonts in them or subdirectories, which can be either in the archive or in a subdirectory. Fonts are determined by a specific name (the presence of a font at the beginning of the name), setopt nullglob allows you not to clue about their absence (by default, the absence would cause an error). Using setopt extendedglob together with (#i) allows you not to worry about the register: (#i) allows fonts to be located both in the FONTS directory and in Fonts . After finding and installing fonts in ~/.fonts indexes are updated using fc-cache : otherwise, even fonts copied to the correct directory will not be used. ${(k)ASSOCIATIVE_ARRAY} turns an associative array into a simple array of keys.

In the third cycle, there are directories with subtitles that have “simple” names like “subs” or “subtitles” and are stuffed into an associative array. Again, ignoring case and separately (/) at the end are used, limiting glob to directories only (for example, glob qualifier). ${^array} used to make array=( abc ); echo ${^array}* array=( abc ); echo ${^array}* was equivalent to echo {a,b,c}* .

The last cycle finds directories with subtitles, called non-standard way. A subtitle directory is considered to be any subdirectory (with respect to video directories) containing at least one file with the ass , ssa or srt extension.

It is necessary to note the presence of a rather strange code: nobody seems to touch the subpaths variable, but its value is used as the argument --sub-paths . The fact is that zsh noted a rather frequent pattern when an array of values ​​(usually directories) is a simple string, where different values ​​are separated from each other by a separator (usually a colon): an example of such an “array” is PATH . However, it would be convenient for programmers to work with such arrays exactly as with arrays, therefore, “linked” variables were created, where one of the array variables (example: path ) and another string with the specified (default colon) separator (example: PATH ) , and the change of one of the variables is automatically reflected in the other. In this way, the SUBPATHS array was associated with the string subpaths .

7. Creation of commands with automatic screening of arguments.


Arguments of some commands are never files. However, this fact does not stop zsh from disclosing templates. In the usual case, it suffices to write alias mycmd='noglob mycmd' and mycmd *.foo will become equivalent to mycmd '*.foo' . But what if you want to create a team, at the input of which you are going to file $VAR literally and do not want to write '$VAR' ? Here I will give an example of code that records zpy import zsh; print(zsh.getvalue("PATH")) zpy import zsh; print(zsh.getvalue("PATH")) equivalent to zpython 'import zsh; print(zsh.getvalue("PATH"))' zpython 'import zsh; print(zsh.getvalue("PATH"))' ; of course, only in interactive mode:
 zshaddhistory() { emulate -L zsh if (( ${+_HISTLINE} && ${#_HISTLINE} )) ; then print -sr -- "${_HISTLINE}" unset _HISTLINE elif (( ${#1} )) ; then print -sr -- "${1%%$'\n'}" fi fc -p } accept-line() { emulate -L zsh if [[ ${BUFFER[1,4]} == "zpy " ]] ; then _HISTLINE=$BUFFER BUFFER="zpython ${(qqq)BUFFER[5,-1]}" fi zle .accept-line } zle -N accept-line 
The main part of the function: when you call the accept-line widget (it is called when you press enter) it determines whether the line starts with zpy and, if so, the line is replaced with zpython … , where … is the screened part of the line after zpy and space. The zshaddhistory function zshaddhistory used to ensure that the source line is in history, not its replacement.
In this way, you can add any nonstandard syntax to zsh.

8. Automatic exclusion of files from globs


Imagine that you have a Vim editor and you want to use it to open all the files in the directory (use the template * ). But besides simple text files in the directory there are many binary files like *.o (object) files that you do not want to open. To do this, instead of just the asterisks, you can write several templates corresponding to the necessary files. Or use an exception pattern ( *~*.o , requires setopt extendedglob ). But with a relatively simple trick, you can automate it:
 filterglob () { local -r exclude_pat="$2" shift local -r cmd="$1" shift local -a args args=( "${@[@]}" ) local -a new_args local -i expandedglobs=0 local first_unexpanded_glob= for ((I=1; I<=$#args; I++ )) do if [[ $args[I] != ${${args[I]}/[*?]} ]] then local initial_arg=${args[I]} args[I]+="~$exclude_pat(N)" new_args=( $~args[I] ) if (( $#new_args )) ; then expandedglobs=1 else if [[ $options[cshnullglob] == off && $options[nullglob] == off ]] ; then if [[ $options[nomatch] == on ]] ; then : ${~${args[I]%\(N\)}} # Will error out. else new_args=( "$initial_arg" ) fi fi if [[ -z $first_unexpanded_glob ]] ; then first_unexpanded_glob=${args[I]%\(N\)} readonly first_unexpanded_glob fi fi args[I,I]=( "${new_args[@]}" ) (( I += $#new_args - 1 )) fi done if [[ $options[cshnullglob] == on && $options[nullglob] == off ]] ; then if (( !expandedglob )) ; then : $~first_unexpanded_glob # Will error out. fi fi "$cmd" "${args[@]}" } alias vim='noglob filterglob "*.o" vim' 
This defines alias, which prohibits zsh (noglob) itself, but uses the function that opens the templates (filterglob) to launch vim itself. But it doesn’t just reveal them, but also complements the template with the exception so that vim * will work as vim *~*.o .
The function uses the following zsh features: ${~var} causes zsh to use a pattern expansion as applied to the value of the var variable and substitutes the result of the pattern expansion instead of the variable itself. array[idx1,idx2]=( $new_array ) removes part of the array from idx1 to idx2 inclusive, inserting the values ​​of the array new_array in place of the deleted elements. The size of the array can change. Constructs of the form : $~var with the comment “Will error out” are needed in order for zsh to show the expected error. At the same time, the function is completed. There are no particular reasons to use this option instead of echo … >&2 , although my like should support catching an error using always (which you are unlikely to use in an interactive session).

Source: https://habr.com/ru/post/272581/


All Articles