Changing the global “path” from within a function? - zsh

My zshenv file has a bunch of lines like
if [[ -d "$HOME/bin" ]]; then
path=($HOME/bin $path)
fi
I thought I’d try to factor this pattern out into a function. I replaced it with
function prepend_to_path_if_exists() {
if [[ -d $1 ]]; then
path=($1 $path)
fi
}
prepend_to_path_if_exists("$HOME/bin")
but this gives the error
/Users/bdesham/.zshenv:8: missing end of string
where line 8 is the one where I’m calling prepend_to_path_if_exists. What exactly is causing this error, and how can I make this function work? I’m using zsh 5.0.5 on OS X 10.10.1.

You could call functions as with usual command executions like this (without ()):
prepend_to_path_if_exists "$HOME/bin"
It seems that zsh try to expand the glob prepend_to_path_if_exists(…) rather than to call the function.
TL;DR: Prepending emelemnts to $path would be accomplished by a little cryptic way:
(I'm not quite sure that the below form is preferable for anyone though.)
# `typeset -U` uniqify the elements of array.
# It could be good for $path.
typeset -U path
# prepending some paths unconditionally,
path[1,0]=(\
$HOME/bin \
$HOME/sbin \
)
# then filtering out unnecessary entries afterward.
path=(${^path}(-/N))
The $path[x,0]=… is prepending(splicing) element(s) to array taken from the below:
So that's the same as VAR[1,0]=(...) ? It doesn't really "look" very
much like prepend to me.
-- Greg Klanderman (http://www.zsh.org/mla/workers/2013/msg00031.html)
The ${^path}(-/N) expands the glob qualifires -/N on the each $path elements.
(Without ^ in the parameter expansion, the last elements of array will be evaluated, so it is mandatory in this case.)
The glob qualifires -/N means that "symbolic links and the files they point to"(-) the "directory"(/). And when it does not match anything do not raise errors (N).
In short, it would keep exsisting directories only for $path.

Related

How to make an alias to enclose multi-member expression passed as argument to a perl script

I spent a long time searching but cannot find the answer.
I am using an "astronomy-aware" perl script which is super useful for calculations on the command line and in scripts. The problem is that parentheses have to be escaped:
calc.pl (1+1)/(2+2)
zsh: unknown file attribute: 2
calc.pl \(1+1\)/\(2+2\)
0.5
The best alternative to escaping each one is using single quotes to enclose the entire expression like this:
calc.pl '(1+1) / (2+2)'
0.5
How can I define a zsh alias (like alias calc="${HOME}/bin/calc.pl") that encloses the expression that comes after the call to the script within the single quotes as shown in the second example?
The solution linked by Barmar works! Thank you so much!
It was provided by (aloxaf) here:
https://superuser.com/questions/1508079/auto-quote-arguments-in-zsh
I defined the following in my .zshrc and it works.
function quote-accept-line() {
local -a starts_with=("calc.pl ")
for str ($starts_with) {
if [[ ${(M)BUFFER#$str} ]] {
BUFFER=$str${(qq)BUFFER#$str}
}
}
zle accept-line
}
zle -N quote-accept-line
# bind it to "Enter"
bindkey "^M" quote-accept-line
The special noglob command modifier can be used for this:
% calc.pl (1+1) / (2+2)
zsh: unknown file attribute: 1
% noglob calc.pl (1+1) / (2+2)
0.5
Typing noglob all the time will probably get boring fast, so you can create an alias for this in your zshrc:
% alias calc.pl='noglob calc.pl'
% calc.pl (1+1) / (2+2)
0.5
The difference between the "auto quoter" is that something like this:
% calc.pl 2' * 3
won't work, as the quotes are still interpreted. I have never run in to issues with this though, as I can't recall ever having used quotes in a calculation, but maybe your Perl script accepts some syntax for that.
Either way, it's a much simpler solution which is probably enough for your purpose.
Bonus: zsh comes with the zcalc module, which provides a calculator; I have no idea how this compares to your Perl script, but the way I have this set up is like this:
autoload -U zcalc # Get quick results for "zc 6 * 6", or just use "zc" to get zcalc
alias zc >/dev/null && unalias zc
zc() { if (( $# )); then zcalc -e ${(j: :)#}; else zcalc; fi }
alias zc='noglob zc'
By default zcalc will throw you in to a REPL unless you use -e, which I find a bit annoying; this way you can type zc (1.0+1) / (2+2) and get your results quickly.
[The question originally asked for a bash solution and was originally tagged bash.]
You're missing the point of the quotes or escapes. It's for the benefit of the shell, not calc.pl. (calc.pl doesn't even see them; it gets (1+1)/(2+2) when you execute the shell command calc.pl \(1+1\)/\(2+2\).)
The issue is that ( ... ) and spaces have special meaning to the shell, so escapes and/or quotes are used to change how the shell interprets them.
You can't do anything about that after the shell has already interpreted them incorrectly, so your request has no solution.
[While the above is still true in zsh, one can hook into zsh's command line editor as shown in the other answer. This allows the command to be edited before zsh sees it.]

Process substitution =(list) in middle of argument

How can I use =(list)-style process substitution in the middle of an argument?
This works:
% echo =(echo)
/tmp/zshxxxxxx
So does this:
% echo =(echo):works
/tmp/zshxxxxxx:works
But this does not:
% echo broken:=(echo)
zsh: missing end of string
Notably, this also works:
% echo works:<(echo)
works:/proc/self/fd/11
The problem is =(list) can only stand at the beginning of arguments. Quoting from the ZSH manual:
The expression may be preceded or followed by other strings except
that, to prevent clashes with commonly occurring strings and patterns,
the last form [this is =(list)] must occur at the start of a command
argument, and the forms are only expanded when first parsing command
or assignment arguments.
I have a tool that accepts an argument of the form format:filename, and I need to use a real file, not a pipe, so I cannot use <(list). What is a reasonably simple and readable solution?
Use parameter expansion to "buffer" the process substitution.
% echo fixed:${:-=(echo)}
fixed:/tmp/zshxxxxxx
I have been trying to use the previous answer for a makefile, and it was not so trivial so here is my solution.
The initial problem is that with MinGW, the command line length is quite limited and it will get truncated in case of a very long object list, so I need to use the #file syntax for gcc, which allow to provide the arguments in a file.
SHELL := /bin/zsh
myprog.exe: very.o long.o list.o of.o obj.o files.o ...
gcc -o $# #$${:-=(<<< \"$^\")}
There is an alternate solution by using an anonymous function called immediatly :
myprog.exe: very.o long.o list.o of.o obj.o files.o ...
() { gcc -o $# #$$1 } =(<<< "$^")

How can I implement the command 'ls' with wildcard, '*'?

EDIT #1 : I'm under the limit that all arguments are enclosed in two quotes, so that shell do not expand any argument with * to the corresponding path.
EDIT #2 : In order to retrieve directories such as */*, ../*, and dirA/*/file.out, How should I use iteration loop or recursive call?
I have just learned about the function fnmatch(). But I don't know start place.
There are many possible cases. I'm confused dealing with these all cases.
For example, Let me assume that executable program is a.out.
$./a.out -l */*
$./a.out -l ../*
$./a.out -l [file_name] [directory_name]
/* Since I also have to implement ls command with no wildcard. */
What should I do? Any advice would be awesome.
Thank you in advance.
Your problem is : shell replaces wildcard caracter * with all of the filenames matching the pattern.
Solution:
If you do not want to use this feature of bash, just put quotation marks around your command line arguments.
Calling your program that way will have the original arguments, containing wildcards.
After this, you can list all the filenames with their paths. For example using some recursive algorithm. Then you can apply some matching to these path string. (when visiting it)
If you want to be a good unix citizen, the rule is Don't do filename globbing unless you are writing a shell.
You want to write an ls-like program? Don't do any wildcard expansion. Don't treat "*" specially. Just treat your argv as a list of filenames. If your program handles these cases:
./a.out file1
./a.out file1 file2 file3
Then it will also handle
./a.out file*
correctly because the shell will do the expansion and your program won't need to know about it. And besides that, it will handle this:
zsh% ./a.out **/file<40-185>~file<90-100>(.mm-30OL[1,2])
which in zsh expanded glob syntax means: expand file40 through file185, except for file90 through file100, include only the ones that have been modified in the last 30 minutes, and use only the largest 2 files in the resulting set.
fnmatch is never going to do anything like that. But these fancy globs can be used with any command that just takes a filename list and doesn't care where it came from.
When you're in a situation where you can't take a list of filenames from the command line, then consider using fnmatch. ls isn't one of those situations.

How can I remove a path from $PATH in Zsh and add it to the beginning without duplication?

I have:
PATH=/bar:/foo
I want:
PATH=/foo:/bar
I don't want:
PATH=/foo:/bar:foo
So I'm thinking, given the default path is PATH=/bar, I can modify $path (which is $PATH as an associative array):
function prepend_to_path() {
unset $path[(r)$1]
path=($1 $path)
}
prepend_to_path /foo
But that complains with:
prepend_to_path:unset:1: not enough arguments
It's been so long that I don't even remember what (r) is for, but without it (unset $path[$1]) I get:
prepend_to_path:1: bad math expression: operand expected at `/home/nerd...'
What am I doing wrong?
You can replace the body of your function with:
path=($1 ${(#)path:#$1})
Related answer: https://stackoverflow.com/a/3435429/1107999
This also works (and is arguably easier to read when you go back to it after a couple of months):
prepend_to_path () {
path[1,0]=$1
typeset -U path
}
typeset -U will automatically deduplicate the array, keeping only the first occurrence of each element.
Since export is equivalent to typeset -gx, you could also export -U path to kill two birds with one stone.
Edit: typeset -U needs only to be applied to a particular array once, so one can do that somewhere in one's shell startup and remove the line from the function above.

How to edit path variable in ZSH

In my .bash_profile I have the following lines:
PATHDIRS="
/usr/local/mysql/bin
/usr/local/share/python
/opt/local/bin
/opt/local/sbin
$HOME/bin"
for dir in $PATHDIRS
do
if [ -d $dir ]; then
export PATH=$PATH:$dir
fi
done
However I tried copying this to my .zshrc, and the $PATH is not being set.
First I put echo statements inside the "if directory exists" function and I found that the if statement was evaluating to false, even for directories that clearly existed.
Then I removed the directory-exists check, and the $PATH was being set incorrectly like this:
/usr/bin:/bin:/usr/sbin:/sbin:
/usr/local/bin
/opt/local/bin
/opt/local/sbin
/Volumes/Xshare/kburke/bin
/usr/local/Cellar/ruby/1.9.2-p290/bin
/Users/kevin/.gem/ruby/1.8/bin
/Users/kevin/bin
None of the programs in the bottom directories were being found or executed.
What am I doing wrong?
Unlike other shells, zsh does not perform word splitting or globbing after variable substitution. Thus $PATHDIRS expands to a single string containing exactly the value of the variable, and not to a list of strings containing each separate whitespace-delimited piece of the value.
Using an array is the best way to express this (not only in zsh, but also in ksh and bash).
pathdirs=(
/usr/local/mysql/bin
…
~/bin
)
for dir in $pathdirs; do
if [ -d $dir ]; then
path+=$dir
fi
done
Since you probably aren't going to refer to pathdirs later, you might as well write it inline:
for dir in \
/usr/local/mysql/bin \
… \
~/bin
; do
if [[ -d $dir ]]; then path+=$dir; fi
done
There's even a shorter way to express this: add all the directories you like to the path array, then select the ones that exist.
path+=/usr/local/mysql/bin
…
path=($^path(N))
The N glob qualifier selects only the matches that exist. Add the -/ to the qualifier list (i.e. (-/N) or (N-/)) if you're worried that one of the elements may be something other than a directory or a symbolic link to one (e.g. a broken symlink). The ^ parameter expansion flag ensures that the glob qualifier applies to each array element separately.
You can also use the N qualifier to add an element only if it exists. Note that you need globbing to happen, so path+=/usr/local/mysql/bin(N) wouldn't work.
path+=(/usr/local/bin/mysql/bin(N-/))
You can put
setopt shwordsplit
in your .zshrc. Then zsh will perform world splitting like all Bourne shells do. That the default appears to be noshwordsplit is a misfeature that causes many a head scratching. I'd be surprised if it wasn't a FAQ. Lets see... yup:
http://zsh.sourceforge.net/FAQ/zshfaq03.html#l18
3.1: Why does $var where var="foo bar" not do what I expect?
Still not sure what the problem was (maybe newlines in $PATHDIRS)? but changing to zsh array syntax fixed it:
PATHDIRS=(
/usr/local/mysql/bin
/usr/local/share/python
/usr/local/scala/scala-2.8.0.final/bin
/opt/local/Library/Frameworks/Python.framework/Versions/2.6/bin
/opt/local/Library/Frameworks/Python.framework/Versions/2.7/bin
/opt/local/etc
/opt/local/bin
/opt/local/sbin
$HOME/.gem/ruby/1.8/bin
$HOME/bin)
and
path=($path $dir)

Resources