Why ever use zsh array-parameters? - zsh

list=(1 2 3)
for i in $list; do echo $i; done;
for i in $list[#]; do echo $i; done;
for i in $list[*]; do echo $i; done;
for i in ${list}; do echo $i; done;
for i in ${list[#]}; do echo $i; done;
for i in ${list[*]}; do echo $i; done;
for i in "${list[#]}"; do echo $i; done;
All of these print the same thing:
1
2
3
These
for i in "$list"; do echo $i; done;
for i in "${list}"; do echo $i; done;
for i in "$list[*]"; do echo $i; done;
for i in "${list[*]}"; do echo $i; done;
all print
1 2 3
When should I use brackets vs. no brackets and # vs. *? For example, I know the difference between "${list[#]}" and "${list[*]}", but I have not been able to find a straightforward answer on the difference between $list, ${list}, $list[#], ${list[#]}, $list[*], and ${list[*]}. Why would I ever use array parameters when I could just do $list?
Similarly, why wouldn't I just "$list" when I want all of the elements in the array in one string? In that case I have, insanely, 4 different options: "$list", "${list}", "$list[*]", "${list[*]}".

In general, it is up to us to which to use. But it could be worthwhile to dig into why zsh permits to write codes like these.
From zsh FAQ documents:
1.2: What is it?
Zsh is a UNIX command interpreter (shell) which of the standard shells most resembles the Korn shell (ksh);
--- Chapter 1: Introducing zsh and how to install it, Z-Shell Frequently-Asked Questions
and,
Chapter 2: How does zsh differ from...?
As has already been mentioned, zsh is most similar to ksh, while many of the additions are to please csh users.
...
2.1: Differences from sh and ksh
Most features of ksh (and hence also of sh) are implemented in zsh; problems can arise because the implementation is slightly different.
--- Chapter 2: How does zsh differ from...?, Z-Shell Frequently-Asked Questions
I tell myself it should be noted that zsh has lots of ksh like features and emulation options when I write my own zsh scripts. You could experiment with setopt ksh_arrays to see what is going on, too.
Arrays are (by default) more csh-like than ksh-like: subscripts start at 1, not 0; array[0] refers to array[1]; $array refers to the whole array, not $array[0]; braces are unnecessary: $a[1] == ${a[1]}, etc. Set the KSH_ARRAYS option for compatibility.
--- 2.1: Differences from sh and ksh, Z-Shell Frequently-Asked Questions
It could give some hints/answers by showing some comparisons with ksh.
brackets vs. no brackets
Q22. Why are the braces required with array references, e. g. ${x[1]}?
A22. It would be nice to do $x[1], but the POSIX shell would expand $x and then search for the file pattern resulting by concatenating [1]. ksh is POSIX compatible.
--- III SHELL PROGRAMMING QUESTIONS Q22, KSH-93 - Frequently Asked Questions
ksh treats array parameters like this:
list=(a b c)
echo $list[1]
;# => "a[1]"
;# concatination of the first $list element and "[1]" string
;# rather than below!
echo ${list[1]} ;# => "b"
So, it could be used any of $list, ${list}, $list[#], ${list[#]}, $list[*], and ${list[*]} in the first example; it could be considered as feature of zsh.
You could look at the codes from another angle by reading above ksh document's $x[1] to $list[#] or $list[*].
Note: In zsh, if $list contains empty value(s), "${list[#]}" differs according to "24. Empty argument removal".
24. Empty argument removal
If the substitution does not appear in double quotes, any resulting zero-length argument, whether from a scalar or an element of an array, is elided from the list of arguments inserted into the command line.
--- 24. Empty argument removal, Rules, Expansion, zshparam(1)
# vs. *
Q1. What is the difference between * and #, for example, and ?
A1. When used outside of "", they are equivalent. However, within double quotes, "$#" produces one argument for each positional parameter, and "$*" produces a single argument. Note that "$#" preserves arguments lists, whereas $* may not unless both word splitting and pathname expansion are disabled.
--- III SHELL PROGRAMMING QUESTIONS Q1, KSH-93 - Frequently Asked Questions
This first half is same as zsh as you know. Here is a same reference for zsh document you suggested:
A subscript of the form [*] or [#] evaluates to all elements of an array; there is no difference between the two except when they appear within double quotes.
"$foo[*]" evaluates to "$foo[1] $foo[2] ...", whereas "$foo[#]" evaluates to "$foo[1]" "$foo[2]" ....
...
When an array parameter is referenced as $name (with no subscript) it evaluates to $name[*],
--- Array Subscripts, zshparam(1)
"$list" vs. others
As you can see it would be already clear, zsh gives us 4 different options as its feature. But I think that ksh user could say that:
"$list", "${list}" and "$list[*]" could mean that it would be doing just some operation on the first element of $list (and the result of concatinating "[*]" for the latter) rather than list/array references.
Here is an example code:
list=(1 2 '' 3) # XXX: added an empty entry to check the difference
test-list-dq () {
echo "$1"
local i=
echo '$list:'; for i in $list; do echo $i; done;
echo '$list[#]:'; for i in $list[#]; do echo $i; done;
echo '$list[*]:'; for i in $list[*]; do echo $i; done;
echo '${list}:'; for i in ${list}; do echo $i; done;
echo '${list[#]}:'; for i in ${list[#]}; do echo $i; done;
echo '${list[*]}:'; for i in ${list[*]}; do echo $i; done;
echo '"${list[#]}":'; for i in "${list[#]}"; do echo $i; done;
}
test-list-nq () {
echo "$1"
local i=
for i in "$list"; do echo $i; done
for i in "${list}"; do echo $i; done
for i in "$list[*]"; do echo $i; done
for i in "${list[*]}"; do echo $i; done
}
echo "*double quotes"
test-list-dq "*default"
() {
setopt localoptions ksharrays no_nomatch
test-list-dq "*ksharrays on"
}
echo "*no quotes"
test-list-nq "*default"
() {
setopt localoptions ksharrays no_nomatch
test-list-nq "*ksharrays on"
}
Outputs below:
*double quotes
*default
$list:
1
2
3
$list[#]:
1
2
3
$list[*]:
1
2
3
${list}:
1
2
3
${list[#]}:
1
2
3
${list[*]}:
1
2
3
"${list[#]}":
1
2
3
*ksharrays on
$list:
1
$list[#]:
1[#]
$list[*]:
1[*]
${list}:
1
${list[#]}:
1
2
3
${list[*]}:
1
2
3
"${list[#]}":
1
2
3
*no quotes
*default
1 2 3
1 2 3
1 2 3
1 2 3
*ksharrays on
1
1
1[*]
1 2 3

Try the same with
list=('a b' $'c\nd')
Also, set $IFS to e.g. '|' to see how ${list[*]} works.
list=('a b' $'c\nd')
IFS='|'
printf %s "${list[*]}"

Related

How to complete a variable number of arguments containing spaces

I've build a command line tool and I need to complete arguments with zsh. I never wrote a zsh completion function so I looked in the scripts provided with zsh but I missed something so that it could work properly.
So, mytool can take a variable number of values and two options.
Here are some call examples:
mytool ONE
mytool ONE TWO
mytool AAA BBB CCC DDD EEE --info
In order to complete the values, I hava another executable that outputs all possible lines to stdout, like this simplified script named getdata:
#!/usr/local/bin/zsh
echo ONE
echo TWO ONE
echo TWO TWO
# ... a lot of lines
echo OTHER ONE
echo ONE ANOTHER LINE
echo AAA BBB CCC DDD EEE
Each completion must match to a whole line, so in my getdata example, it will not be possible to just complete with the value TWO because this whole line does not exist, it must be TWO ONE or TWO TWO.
As this script is quite time consuming, I would like to use zsh caching feature. So, here is my zsh complete script:
compdef _complete_mytool mytool
__mytool_caching_policy() {
oldp=( "$1"(Nmh+1) ) # 1 hour
(( $#oldp ))
}
__mytool_deployments() {
local cache_policy
zstyle -s ":completion:${curcontext}:" cache-policy cache_policy
if [[ -z "$cache_policy" ]]; then
zstyle ":completion:${curcontext}:" cache-policy __mytool_caching_policy
fi
if ( [[ ${+_mytool_values} -eq 0 ]] || _cache_invalid mytool_deployments ) \
&& ! _retrieve_cache mytool_deployments;
then
local -a lines
_mytool_values=(${(f)"$(_call_program values getdata)"})
_store_cache mytool_deployments _mytool_values
fi
_describe "mytool values" _mytool_values
}
_complete_mytool() {
integer ret=1
local -a context expl line state state_descr args
typeset -A opt_args
args+=(
'*:values:->values'
'--help[show this help message and exit]'
'(-i --info)'{-i,--info}'[display info about values and exit]'
'(-v --version)'{-v,--version}'[display version about values and exit]'
)
_call_function res __mytool_deployments
return ret
}
But when I try to complete, spaces are escaped with backslash, and I don't want this behaviour.
mytool OTHER\ ONE
The options seem not to be completed too... So, any help will be greatly appreciated.
Thanks to okdana on the freenode zsh channel who helped me a lot.
So, the solution is:
compdef _complete_mytool mytool
__mytool_caching_policy() {
oldp=( "$1"(Nmh+1) ) # 1 hour
(( $#oldp ))
}
__mytool_deployments() {
local cache_policy
zstyle -s ":completion:${curcontext}:" cache-policy cache_policy
if [[ -z "$cache_policy" ]]; then
zstyle ":completion:${curcontext}:" cache-policy __mytool_caching_policy
fi
if ( [[ ${+_mytool_values} -eq 0 ]] || _cache_invalid mytool_deployments ) \
&& ! _retrieve_cache mytool_deployments;
then
local -a lines
_mytool_values=(${(f)"$(_call_program values getdata)"})
_store_cache mytool_deployments _mytool_values
fi
_describe "mytool values" _mytool_values -Q
}
_complete_mytool() {
_arguments : \
': :__mytool_deployments' \
'--help[show this help message and exit]' \
'(-i --info)'{-i,--info}'[display info about values and exit]' \
'(-v --version)'{-v,--version}'[display version about values and exit]'
}

Break the nested while loops in unix scripting

Have two files:
file1 is having the key words - INFO ERROR
file2 is having the list of log files path - path1 path2
I need to exit out of the script if any of the condition in any of the loops failed.
Here is the Code:
#!/bin/bash
RC=0
while read line
do
echo "grepping from the file $line
if [ -f $line ]; then
while read key
do
echo "searching $key from the file $line
if [ condition ]; then
RC=0;
else
RC=1;
break;
fi
done < /apps/file1
else
RC=1;
break;
fi
done < apps/file2
exit $RC
Thank you!
The ansewer to your question is using break 2:
while true; do
sleep 1
echo "outer loop"
while true; do
echo "inner loop"
break 2
done
done
I never use this, it is terrible when you want to understand or modify the code.
Already better is using a boolean
found_master=
while [ -n "${found_master}" ]; do
sleep 1
echo "outer loop"
while true; do
echo "inner loop"
found_master=true
break
done
done
When you do not need the variable found_master it is an ugly additional variable.
You can use a function
inner_loop() {
local i=0;
while ((i++ < 5)); do
((random=$RANDOM%5))
echo "Inner $i: ${random}"
if [ ${random} -eq 0 ]; then
echo "Returning 0"
return 0
fi
done;
return 1;
}
j=0
while ((j++ < 5 )); do
echo "Out loop $j"
inner_loop
if [ $? -eq 0 ]; then
echo "inner look broken"
break
fi
done
But your original problem can be handles without two while loops.
You can use grep -E "INFO|ERROR" file2 or combining the keywords. When the keywords are on different lines in file1, you can use grep -f file1 file2.
Replace condition with $(grep -c ${key} ${line}) -gt 0 like this:
echo "searching $key from the file $line
if [ $(grep -c ${key} ${line}) -eq 0 ]; then
It will count the each key-word in your log-file. If count=0 (pattern didn't found), running then. If found at least 1 key, running else, RC=1 and exit from loop.
And be sure, that your key-words can't be substrings of the longest words, or you will get an error.
Example:
[sahaquiel#sahaquiel-PC Stackoverflow]$ cat file
correctstringERROR and more useless text
ERROR thats really error string
[sahaquiel#sahaquiel-PC Stackoverflow]$ grep -c ERROR file
2
If you wish to avoid count 2 (because counting first string, obliviously, bad way), you should also add two keys for grep:
[sahaquiel#sahaquiel-PC Stackoverflow]$ grep -cow ERROR file
1
Now you have counted only the words equal to your key, not substrings in any useful strings.

Keep getting error in if statement in UNIX Script

I have had to write a script that can execute 2 command line arguments to execute task. In addition to a while-loop. I am having trouble with the if statement on line 16. The shell produces the following:
./asg6s: line 16: syntax error near unexpected token fi'
'/asg6s: line 16:fi
My code is as follows:
#check if number of arguments are 2
if [ $# -ne 2 ]; then
echo "Does not equal two arguments"
echo "Usage $0 inputfile outputfile"
exit 1
fi
# check if input file exists
if [ ! -e $1 ]; then
echo "$1 not found!"
exit 1
fi
#check if input file is empty
if [ ! -s $FILE ] ; then
echo "$1 is empty"
exit 1
fi
# copy contents of first file to second
cat $1 > $2
while true
do
clear
# display the menu
echo "University of Maryland."
echo "purpose of using the app"
echo -en '\n'
echo "Choose one of the following:"
echo "1 Addition"
echo "2 Subtraction"
echo "3 Multiplication"
echo "4 Division"
echo "5 Modulo"
echo "0 Exit"
echo -en '\n'
#take input for operation
read N
case $N in
1) NAME="add";OP="+";;
2) NAME="subtract";OP="-";;
3) NAME="multiply";OP="*";;
4) NAME="divide";OP="/";;
5) NAME="modulo";OP="%";;
0) echo "The progam is ending" ; exit 0;;
*) echo “Not an Acceptable entry.” ;continue;
esac
#take input numbers
echo "Enter two numbers"
read A
read B
#display value on screen and also append in the output file
echo "The operation is to $NAME. The result of $NAME $A and $B is" `expr $A $OP $B`
echo "The operation is to $NAME. The result of $NAME $A and $B is" `expr $A $OP $B` > $2
done
Any help would be appreciated.
Edit:
In the same code above I have been getting a problem with the loop statement. Well I should say the fact that I cannot get the program to print the answer for me after I input to integers into the program. Specifically, it does nothing and goes back to the point where it ask me input for what operation I want to complete. Any help would be appreciated.
Your script is almost working.
After showing the result of the expr your script continues with the while loop and calls clear. If you want to see the result, you must show the result after the clear or read a dummy key input.
Another problem is the variable $OP, that could be a *. When * is evaluated to a set of files, your expr statement will not work.
The shortest changes is adding a read statement and quoting your $OP:
echo "The operation is to $NAME. The result of $NAME $A and $B is" `expr $A "$OP" $B`
echo "The operation is to $NAME. The result of $NAME $A and $B is" `expr $A "$OP" $B` > $2
read dummy
Of course the script can be changed. Do you really want to overwrite $2 with the results or append to the file?
The 2 echo lines can be put together with tee.
I would move the clear to above the while statement and replace the last part of your script with
read A
read B
clear
#display value on screen and also append in the output file
echo "The operation is to ${NAME}. The result of ${NAME} $A and $B is $(expr $A "${OP}" $B)" | tee -a $2
echo
done

last n elements of an array

I'm trying to write a function that will print out the last 3 elements of $PWD, with a '...' beforehand if there are more than 3 elements.
e.g.
/home/nornagon/src --> ~/src
/home/nornagon/src/foo/bar/baz --> ...foo/bar/baz
This is my code so far, but $foo[-3,-1] doesn't work if the array has too few elements in it.
function custom_pwd() {
d=${PWD/#$HOME/\~}
d=(${(s:/:)d})
echo $d[-4,-1]
}
The zsh already has some nifty prompt processing available with print's -P option. This should do the trick:
custom_pwd() {
d=$(print -P '%3~')
case $d in
('~'*|/*) echo "$d";;
(*) echo "...$d"
esac
}
See man zshmisc, section "EXPANSION OF PROMPT SEQUENCES" for the gory details.
Here's what I came up with, though it's not terribly elegant:
function custom_pwd() {
local d slash
d=${PWD/#$HOME/\~}
case $d in
/*) slash=/ ;;
*) slash= ;;
esac
d=(${(s:/:)d})
d[1]=$slash$d[1]
num=$#d
ellipsis=
if (( num > 3 )); then num=3; ellipsis='…'; fi
echo $ellipsis${(j./.)d[-$num,-1]}
}

How to use loops statements in unix shell scripting

How to use loop statements in unix shell scripting for eg while ,for do while. I'm using putty server.
for: Iterate over a list.
$for i in `cat some_file | grep pattern`;do echo $i;done
while loop looks pretty much like C's.
$ i=0;while [ $i -le 10 ];do echo $i;i=`expr $i + 1` ;done
If you are going to use command line only, you could use perl, but I guess this is cheating.
$perl -e '$i=0;while ($i < 10){print $i;$i++;}'
More data
http://www.freeos.com/guides/lsst/
#!/bin/sh
items=(item1 item2 item3)
len=${#items[*]}
i=0
while [ $i -lt $len ]; do
echo ${items[$i]}
let i++
done
exit 0
As well as the 'for' and 'while' loops mentioned by Tom, there is (in classic Bourne and Korn shells at least, but also in Bash on MacOS X and presumably elsewhere too) an 'until' loop:
until [ -f /tmp/sentry.file ]
do
sleep 3
done
This loop terminates when the tested command succeeds, in contrast to the 'while' loop which terminates when the tested command fails.
Also note that you can test a sequence of commands; the last command is the one that counts:
while x=$(ls); [ -n "$x" ]
do
echo $x
done
This continues to echo all the files in the directory until they're all deleted.
to the OP, to iterate over files
for file in *
do
echo "$file"
done
to generate counters
for c in {0..10}
do
echo $c
done
using for loop
max=10
for (( i=0; i<=$max; i++ ));
do
echo $i
done
to iterate through a file in KSH
while read line ; do
echo "line from file $line"
done < filename.txt
echo "sample while loop"
i=0;
while [ $i -le 10 ]
do
echo $i
(( i++ ))
done

Resources