loop over output of command and print both index and value - bash

I have checked Looping over arrays, printing both index and value. The issue is I want to loop over output of command and not an array.
The code i came up with is:
array=($(seq 1 10))
for i in "${!array[#]}"; do
printf "%s\t%s\n" "$i" "${array[$i]}"
done
or,
ITER=0
for I in $(seq 1 10)
do
echo ${I} ${ITER}
ITER=$(expr $ITER + 1)
done
What i want to know is, is it possible to do it within the loop only (without array or ITER) outside the loop?
What i am looking for is something like:
for index,value in $(seq 1 10); do
echo $index $value
done

Let us know your actual requirement:
01)
#!/bin/bash
index=0
for filename in $(ls -atr)
do
indx=$(($indx+1))
echo "Index: $indx $filename"
done
output:
$ ./73412398.sh
Index: 1 ..
Index: 2 73412398.sh
Index: 3 .
One more try:
for index in $(ls -atr | grep -n $)
do
echo $index | sed "s/\([0-9]*\):/\1 /;"
done
output:
1 ..
2 73412398.sh
3 .

after modifying murugesan openssl's answer, the solution for me is:
for indexval in $(ls -atr | grep -n $)
do
echo index is "${indexval%%:*}"
echo value is "${indexval#*:}"
done

Related

Bash script to add absolute values of numbers seperated by spaces

I need a bash script to find the sum of the absolute value of integers separated by spaces. For instance, if the input is:
1 2 -3
the script should print 6 to standard output
I have:
while read x ; do echo $(( ${x// /+} )) ; done
which gives me
0
Without over complicated things, how would I include an absolute value of each x in that statement so the output would be:
6
With Barmar's idea:
echo "1 2 -3" | tr -d - | tr ' ' '+' | bc -l
Output:
6
You have almost done it, but the -s must have been removed from the line read:
while read x; do x=${x//-}; echo $(( ${x// /+} )); done
POSIX friendly implementation without running a loop and without spawning a sub-shell:
#!/usr/bin/env sh
abssum() {
IFS='-'
set -- $*
IFS=' '
set -- $*
IFS=+
printf %d\\n $(($*))
}
abssum 1 2 -3
Result:
6

shell - iterating a loop for a (validated) supplied number or range of numbers

I need to accept input from user (i.e. 'read').
This input can be either a single positive number or a range of numbers (in the form X-Y ).
I then need to validate this input and perform an iterative loop through the range of numbers (or just once in the case of only X).
examples:
1) User supplies: "8" or "8-"
Loop runs only a single time supplying the number 8
2) User supplies: "13-22"
Loop runs 11 times (13 through 22) referencing the number 13.
3) User supplies: "22-13"
Probably should behave like #2 above...but I'm open to other clean ways to validate the input.
I have worked on the following so far, which isn't very clean, complete, or even 100% correct for what I was asking - but it shows the effort and idea I'm going for:
echo "line?"; read -r deleteline
case "$deleteline" in
''|*[!0-9\-]*) echo "not a number";;
[1-9]|[1-9][0-9]*);;
esac
deleteline_lb=$(echo $deleteline|awk -F "-" '{print $1}')
deleteline_ub=$(echo $deleteline|awk -F "-" '{print $2}')
if [ ! $deleteline_lb = "" ] && [ ! "$deleteline_ub" = "" ]; then
delete_line_count=1
delete_line_count=$(expr $deleteline_ub - $deleteline_lb)
if [ $delete_line_count -le 0 ]; then
delete_line_count=1
fi
fi
i=1; while [ $i -le $delete_line_count ]; do
echo $deleteline_lb $i
i=$(($i + 1))
done
This needs to run in sh, things like seq are not supported - so stick with posix compliant methods...
To clarify I am looking to do the following (pseudo-code):
1) accept input from user
2) validate if input is in the form "#" or "#-#" (range).
3) Execute chosen (arbitrary) code path based on proper/improper input.
4) If single # is given then store that to variable to perform future operations against.
5) If range is given, store both numbers in variable to be able to perform the operation against the lower # up to the higher number. More specifically it would be "(higher #) - (lower #) + 1". So if range were 12-17 then we need to perform operation against 12, 6x. (17 - 12 + 1). IOW, 12-17 inclusive.
6) A way to easily denote if data set is range vs single number is also desired so that code path to each can be easily branched.
thanks for helping!
UPDATE:
Using my basic code I reworked it (with a bit of input from a friend), and basically came up with this:
while true;do
printf "\\delete lines? [e=Exit] ";read -r deleteline
case "$deleteline" in
[Ee]) break;;
*)
echo "$deleteline" | egrep -q '^[[:digit:]-]*$'
if [ $? -ne 0 ]; then
printf "\\n input is not a number.\\n"
else
delete_range_start=`echo $deleteline|awk -F "-" '{print $1}'`
delete_range_end=`echo $deleteline|awk -F "-" '{print $2}'`
if [ $delete_range_end -lt $delete_range_start ]; then
printf "\\n upper range must be higher than lower range.\\n"
else
if [ "$delete_range_end" = "" ]; then
delete_range_end=$delete_range_start
elif [ $delete_range_end -gt $lineNumbers ]; then
printf "\\Setting range to last entry\\n"
fi
break
fi
fi
;;
esac
done
deleteline=$delete_range_start
deleteloop=`expr $delete_range_end - $delete_range_start + 1`
i=1
while [ $i -le $deleteloop ]; do
# Insert all processing code in here
i=`expr $i + 1`
done
If you have a posix compliant awk, try this:
echo "$userInput" | awk -F- '
($1+0==$1&&$2+0==$2){
for(i=$1;($1<$2?i<=$2:i>=$2);)
print ($1<$2?i++:i--);
next
}
$1+0==$1{
print $1;
next
}
$2+0==$2{
print $2;
next
}
($1+0!=$1&&$2+0!=$2){
exit 1
}'
The script check if the 2 fields (separated with -) are numbers. If so, it prints these numbers in an ascending or descending way depending if the first number is greater or lower than the second one.
If only one input, the script just prints it.
If none of the field are number, it exits with a non zero value.
This script could be the validation step of a shell script like this:
$ cat test.sh
#!/bin/sh
echo -n "range: "
read -r range
validated_input=$(echo "$range" | awk -F- '($1+0==$1&&$2+0==$2){for(i=$1;($1<$2?i<=$2:i>=$2);)print ($1<$2?i++:i--);next}$1+0==$1{print $1;next}$2+0==$2{print $2;next}($1+0!=$1&&$2+0!=$2){exit 1}')
if [ $? -ne 0 ]; then
echo "Incorrect range" >&2
fi
for i in $validated_input; do
echo "$i"
done
Examples:
$ ./test.sh
range: 10-6
10
9
8
7
6
$ ./test.sh
range: 8-
8
$ ./test.sh
range: hello
Incorrect range

Bash syntax errors like `syntax error: operand expected (error token is "{1 % 2 ")`

Please tell why printing odd numbers in bash script with the following code gives the error:
line 3: {1 % 2 : syntax error: operand expected (error token is "{1 % 2 ")
for i in {1 to 99}
do
rem=$(( $i % 2 ))
if [$rem -neq 0];
then
echo $i
fi
done
This is working example:
for i in {1..99}
do
rem=$(($i % 2))
if [ "$rem" -ne "0" ]; then
echo $i
fi
done
used for loop have a typo in minimum and maximum number, should be {1..99} instead of {1 to 99}
brackets of the if statement needs to be separated with whitespace character on the left and on the right side
Comparision is done with ne instead of neq, see this reference.
As already pointed out, you can use this shell checker if you need some clarification of the error you get.
Not really sure why nobody included it, but this works for me and is simpler than the other 'for' solutions:
for (( i = 1; i < 100; i=i+2 )); do echo $i ; done
To print odd numbers between 1 to 99
seq 1 99 | sed -n 'p;n'
With GNU seq, credit to gniourf-gniourf
seq 1 2 99
Example
$ seq 1 10 | sed -n 'p;n'
1
3
5
7
9
if you reverse it will print even
$ seq 1 10 | sed -n 'n;p'
2
4
6
8
10
One liner:
for odd in {1..99..2}; do echo "${odd}"; done
Or print in a cluster.
for odd in {1..99..2}; do echo -n " ${odd} "; done
Likewise, to print even numbers only:
for even in {2..100..2}; do echo "${even}"; done
OR
for even in {2..100..2}; do echo -n " ${even} "; done
Replace {1 to 99} by {1..99}.
for (( i=1; i<=100; i++ ))
do
((b = $i % 2))
if [ $b -ne 0 ]
then
echo $i
fi
done
for i in {1..99}
do
rem=`expr $i % 2`
if [ $rem == 1 ]
then
echo "$i"
fi
done
for i in {0..49}
do
echo $(($i*2+1))
done

issue with if statement in bash

I have issue with an if statement. In WEDI_RC is saved log file in the following format:
name_of_file date number_of_starts
I want to compare first argument $1 with first column and if it is true than increment number of starts. When I start my script it works but just with one file, eg:
file1.c 11:23:07 1
file1.c 11:23:14 2
file1.c 11:23:17 3
file1.c 11:23:22 4
file2.c 11:23:28 1
file2.c 11:23:35 2
file2.c 11:24:10 3
file2.c 11:24:40 4
file2.c 11:24:53 5
file1.c 11:25:13 1
file1.c 11:25:49 2
file2.c 11:26:01 1
file2.c 11:28:12 2
Every time when I change file it begin counts from 1. I need to continue with counting when it ends.
Hope you understand me.
while read -r line
do
echo "line:"
echo $line
if [ "$1"="$($line | grep ^$1)" ]; then
number=$(echo $line | grep $1 | awk -F'[ ]' '{print $3}')
else
echo "error"
fi
done < $WEDI_RC
echo "file"
((number++))
echo $1 `date +"%T"` $number >> $WEDI_RC
There are at least two ways to resolve the problem. The most succinct is probably:
echo "$1 $(date +"%T") $(($(grep -c "^$1 " "$WEDI_RC") + 1))" >> "$WEDI_RC"
However, if you want to have counts for each file separately, you can do that using an associative array, assuming you have Bash version 4.x (not 3.x as is provided on Mac OS X, for example). This code assumes the file is correctly formatted (so that the counts do not reset to 1 each time the file name changes).
declare -A files # Associative array
while read -r file time count # Split line into three variables
do
echo "line: $file $time $count" # One echo - not two
files[$file]="$count" # Record the current maximum for file
done < "$WEDI_RC"
echo "$1 $(date +"%T") $(( ${files[$1]} + 1 ))" >> "$WEDI_RC"
The code uses read to split the line into three separate variables. It echoes what it read and records the current count. When the loop's done, it echoes the data to append to the file. If the file is new (not mentioned in the file yet), then you will get a 1 added.
If you need to deal with the broken file as input, then you can amend the code to count the number of entries for a file, instead of trusting the count value. The bare-array reference notation used in the (( … )) operation is necessary when incrementing the variable; you can't use ${array[sub]}++ with the increment (or decrement) operator because that evaluates to the value of the array element, not its name!
declare -A files # Associative array
while read -r file time count # Split line into three variables
do
echo "line: $file $time $count" # One echo - not two
((files[$file]++)) # Count the occurrences of file
done < "$WEDI_RC"
echo "$1 $(date +"%T") $(( ${files[$1]} + 1 ))" >> "$WEDI_RC"
You can even detect whether the format is in the broken or fixed style:
declare -A files # Associative array
while read -r file time count # Split line into three variables
do
echo "line: $file $time $count" # One echo - not two
if [ $((files[$file]++)) != "$count" ]
then echo "$0: warning - count out of sync: ${files[$file]} vs $count" >&2
fi
done < "$WEDI_RC"
echo "$1 $(date +"%T") $(( ${files[$1]} + 1 ))" >> "$WEDI_RC"
I don't get exactly what you want to achieve with your test [ "$1"="$($line | grep ^$1)" ] but it seems you are checking that the line start with the first argument.
If it is so, I think you can either:
provide the -o option to grep so that it print just the matched output (so $1)
use [[ "$line" =~ ^"$1" ]] as test.

Unexpected behaviour of for

Script:
#!/bin/bash
IFS=','
i=0
for j in `cat database | head -n 1`; do
variables[$i]=$j
i=`expr $i + 1`
done
k=0
for l in `cat database | tail -n $(expr $(cat database | wc -l) - 1)`; do
echo -n $k
k=`expr $k + 1`
if [ $k -eq 3 ]; then
k=0
fi
done
Input file
a,b,c
d,e,f
g,e,f
Output
01201
Expected output
012012
The question is why the for skips last echo? It is weird, because if I change $k to $l echo will run 6 times.
Update:
#thom's analysis is correct. You can fix the problem by changing IFS=',' to IFS=$',\n'.
My original statements below may be of general interest, but do not address the specific problem.
If accidental shell expansions were a concern, here's how the loop could be rewritten (assuming it's practical to read everything into an array variable first):
IFS=$',\n' read -d '' -r -a fields < <(echo $'*,b,c\nd,e,f\ng,h,i')
for field in "${fields[#]}"; do
  # $field is '*' in 1st iteration, then 'b', 'c', 'd',...
done
Original statements:
Just a few general pointers:
You should use a while loop rather than for to read command output - see http://mywiki.wooledge.org/BashFAQ/001; the short of it: with for, the input lines are subject to various shell expansions.
A missing iteration typically stems from the last input line missing a terminating \n (or a separator as defined in $IFS). With a while loop, you can use the following approach to address this: while read -r line || [[ -n $line ]]; do …
For instance, your 2nd for loop could be rewritten as (using process substitution as input to avoid creating a subshell with a separate variable scope):
while read -r l || [[ -n $l ]]; do …; done < <(cat database | tail -n $(expr $(cat database | wc -l) - 1))
Finally, you could benefit from using modern bashisms: for instance,
k=`expr $k + 1`
could be rewritten much more succinctly as (( ++k )) (which will run faster, too).
Your code expects after EVERY read variable a comma but you only give this:
a,b,c
d,e,f
g,e,f
instead of this:
a,b,c,
d,e,f,
g,e,f,
so it reads:
d,e,f'\n'g,e,f
and that is equal to 5 values, not 6

Resources