Zsh read output of command into array splitting on newline - zsh

I have a command that outputs a bunch of stuff e.g. running mycmd will give:
foobar
derp derp
like so
etc
Some of these lines will have spaces in them.
How do I read these into an array in zsh such that ${arr[1]} gives foobar, ${arr[2]} gives derp derp etc.
I have tried something like but it seems to split the array on chars not newlines.
IFS=$'\n' read -d '' -r arr <<< "$(mycmd)"
i.e. ${arr[1]} gives f when it should give foobar

Okay its actually very simple:
IFS=$'\n' arr=($(mycmd))

I'm not sure exactly why the read usage in the original question didn't work. It's possibly related to mixing <<< and $(). Or maybe the user just had a messed up shell session. Or maybe it was a bug in an older version of Zsh.
In any case, it has nothing to do with the behavior of the read builtin, and the original proposal was very close to correct. The only problem was using <<< $(...) instead of a plain pipe, which should just be a stylistic goof (rather than an error).
The following works perfectly fine in Zsh 5.8.1 and 5.9:
function mycmd {
print foobar
print derp derp
print like so
print etc
}
typeset -a lines
mycmd | IFS=$'\n' read -r -d '' -A lines
echo ${(F)lines}
You should see:
foobar
derp derp
like so
etc
I prefer this style, instead of ( $(...) ). Not requiring a subshell is useful in many cases, and the quoting/escaping situation is a lot simpler.
Note that -d '' is required to prevent read from terminating at the first newline.
You can wrap this up in a function easily:
function read-lines {
if (( $# != 1 )); then
print -u2 'Exactly 1 argument is required.'
return 2
fi
local array="${1:-}"
read -r -d '' "$array"
}

Related

Avoid variable expansion in zsh

If I use the zsh shell and execute the following command I get
$zsh
$echo '$_GET["test"]'
preexec: bad math expression: operand expected at `"test"'
$echo '$_GET[]'
preexec: invalid subscript
In bash I get what I expect:
$bash
$echo '$_GET["test"]'
$_GET["test"]
I assume that zsh is trying to expand the $_GET variable. How can I avoid this? I always expected this to only happen within double quotes anyhow.
[update]
I found the following three lines in the .zshrc:
# Display last command interminal
echo -en "\e]2;Parrot Terminal\a"
preexec () { print -Pn "\e]0;$1 - Parrot Terminal\a" }
After commenting them out everything seems to work as expected.
What I understand is that preexec is executed after a command in the terminal has been submitted but before it is executed. The $1 is the command that one submitted.
I still do not understand the purpose of the two lines but is it because of the double quotes in the preexec print statement that the variables are expanded?
The combination of print -P together with the expansion of $1 is killing you. With this, you first get a "normal" expansion of $1, yielding something like "\e]0;echo '$_GET["test"]'...". Now -P causes print to do a prompt expansion on this string, which means that it has to expand $_GET["test"] as well. This causes the error.
I suggest to remove the -P, in particular since you don't have any characters in your string which would benefit from prompt expansion.

adding a function to .zshrc changes the command

I added the following function to my .zshrc
function jptt(){
# Forwards port $1 into port $2 and listens to it
ssh -N -f -L localhost:$2:localhost:$1 remoteuser#remotehost
}
then I am running jptt 1 2
and get the following error:
Bad local forwarding specification localhost:2ocalhost:1
It is strange that I lose :l after the 2
the function is working when as I tried to replace the command with a simple line and it worked. I also run the ssh command separately and it works well.
The expression $x:l applies the lower-casing modifier to your x variable. The following example illustrates this:
pax> x=ABC
pax> echo $x:lnnn
abcnnn
pax> echo ${x}:lnnn
ABC:lnnn
The first section gives you the lower-case variant, and therefore the modifier is not considered part of your output string. The second section shows how you can prevent this variable expansion by using braces to ensure the :l is not treated as a modifier. In your specific case, that would be done with the line:
ssh -N -f -L localhost:${2}:localhost:${1} remoteuser#remotehost
It's actually a good idea to get into the habit of bracing parameter names as much as possible since there are other cases where this might adversely affect you.
Use ${1} and ${2} . Zsh supports : csh string modificators and :l has special meaning (to lowercase variable in front of it) and that's why it is consumed from $1:localhost.

What is different about these two zsh completion methods?

I have created a zsh completion script. It works as expected if I hardcode the options list, but if I try to generate that same hardcoded value using a bash command, my results are unexpected. For the purposes of this question, I have combined both methods into the same completion script. "silly" is the strange one. "silly2" behaves as expected.
#compdef silly
_silly() {
local -a options
options=($(cat in_file.txt))
_describe 'silly command' options && ret=0
return ret
}
_silly2() {
local -a options
options=("one:Does the 1 thing" "two:Does the 2 thing")
_describe 'silly2 command' options && ret=0
return ret
}
compdef _silly silly
compdef _silly2 silly2
#contents of in_file.txt
#"one:Does the 1 thing" "two:Does the 2 thing"
As you can see, the second method (hardcoded) takes the input string "one:Does..." while the first method (dynamic) generates the exact same output via a cat call to the file in_file.txt, which has identical text.
Ideally, the result looks like this:
one -- Does the 1 thing
two -- Does the 2 thing
The second one does indeed produce this result. The first one, however, produces a result like so:
"two "one -- Does
1 2 the thing"
I cannot for the life of me figure out what the difference is here!
And here I am answering my own question. The answer comes from a related question: How to pass the contents of a file using `cat` to `_values` (zsh completion)
Basically, the spaces are treated differently if they come from the result of a bash command rather than a string literal. The solution is to change the bash internal field separator for the duration of the command, then change it back.
OLD_IFS=$IFS
IFS=$'\n'
options=($(cat in_file.txt))
_describe 'silly command' options && ret=0
IFS=$OLD_IFS
This makes the two command behave the same way.

Combining file tests in Zsh

What is the most elegant way in zsh to test, whether a file is either a readable regular file?
I understand that I can do something like
if [[ -r "$name" && -f "$name" ]]
...
But it requires repeating "$name" twice. I know that we can't combine conditions (-rf $name), but maybe some other feature in zsh could be used?
By the way, I considered also something like
if ls ${name}(R.) >/dev/null 2>&1
...
But in this case, the shell would complain "no matches found", when $name does not fulfil the criterium. Setting NULL_GLOB wouldn't help here either, because it would just replace the pattern with an empty string, and the expression would always be true.
In very new versions of zsh (works for 5.0.7, but not 5.0.5) you could do this
setopt EXTENDED_GLOB
if [[ -n $name(#qNR.) ]]
...
$name(#qNR.) matches files with name $name that are readable (R) and regular (.). N enables NULL_GLOB for this match. That is, if no files match the pattern it does not produce an error but is removed from the argument list. -n checks if the match is in fact non-empty. EXTENDED_GLOB is needed to enable the (#q...) type of extended globbing which in turn is needed because parenthesis usually have a different meaning inside conditional expressions ([[ ... ]]).
Still, while it is indeed possible to write something up that uses $name only once, I would advice against it. It is rather more convoluted than the original solution and thus harder to understand (i.e. needs thinking) for the next guy that reads it (your future self counts as "next guy" after at most half a year). And at least this solution will work only on zsh and there only on new versions, while the original would run unaltered on bash.
How about make small(?) shell functions as you mentioned?
tests-raw () {
setopt localoptions no_ksharrays
local then="$1"; shift
local f="${#[-1]}" t=
local -i ret=0
set -- "${#[1,-2]}"
for t in ${#[#]}; do
if test "$t" "$f"; then
ret=$?
"$then"
else
return $?
fi
done
return ret
}
and () tests-raw continue "${#[#]}";
or () tests-raw break "${#[#]}";
# examples
name=/dev/null
if and -r -c "$name"; then
echo 'Ok, it is a readable+character special file.'
fi
#>> Ok, it is...
and -r -f ~/.zshrc ; echo $? #>> 0
or -r -d ~/.zshrc ; echo $? #>> 0
and -r -d ~/.zshrc ; echo $? #>> 1
# It could be `and -rd ~/.zshrc` possible.
I feel this is somewhat overkill though.

How to read 1 symbol in zsh?

I need to get exactly one character from console and not print it.
I've tried to use read -en 1 as I did using bash. But this doesn't work at all.
And vared doesn't seem to have such option.
How to read 1 symbol in zsh? (I'm using zsh v.4.3.11 and v.5.0.2)
read -sk
From the documentation:
-s
Don’t echo back characters if reading from the terminal. Currently does not work with the -q option.
-k [ num ]
Read only one (or num) characters. All are assigned to the first name, without word splitting. This flag is ignored when -q is present. Input is read from the terminal unless one of -u or -p is present. This option may also be used within zle widgets.
Note that despite the mnemonic ‘key’ this option does read full characters, which may consist of multiple bytes if the option MULTIBYTE is set.
If you want your script to be a bit more portable you can do something like this:
y=$(bash -c "read -n 1 c; echo \$c")
read reads from the terminal by default:
% date | read -sk1 "?Enter one char: "; echo $REPLY
Enter one char: X
Note above:
The output of date is discarded
The X is printed by the echo, not when the user enters it.
To read from a pipeline, use file descriptor 0:
% echo foobar | read -rk1 -u0; echo $REPLY
f
% echo $ZSH_VERSION
5.5.1
Try something like
read line
c=`echo $line | cut -c1`
echo $c

Resources