Doing a loop only 5 times - bash

I want to do a loop (for file in *) only 5 times (so it's not a real loop anymore but however) is there anyway to do this?

Put the files in an array, then slice the array.
$ files=(*)
$ for file in "${files[#]::5}" ; do echo "$file" ; done
あいうえお
0000000000-11-005978.txt
0000000000-11-020832.txt
1
,123

This will only look at the first five items in the directory:
for file in $(ls | head -5)
As Ignacio Vazquez-Abrams points-out, this only works if your filenames don't contain any whitespace. (They likely won't, but something to keep in mind.)

Assuming the variable i is undefined or 0 when you enter the loop and is not used in the loop, just add the line:
test $((++i)) -ge 5 && break
in the loop body. The loop will break out during the 5th iteration, so if you put the line at the end of the loop body, your commands will execute 5 times. If your shell supports it, you can also use the less portable
((++i >= 5)) && break

Here is yet another solution, which only uses bash and no external tools:
let COUNT=0
for FILENAME in *
do
echo do something to $FILENAME
let COUNT=COUNT+1
if (( $COUNT == "5" )); then
break
fi
done

Related

continue <n> not skipping <n> iterations forward in shell script

I have created a hex to ASCII converter for strings in bash. The application I'm on changes characters (anything but [0-9],[A-Z],[a-z]) , in a string to its corresponding %hexadecimal. Eg: / changes to %2F in a string
I want to retain the ASCII characters as it is. Below is my code:
NAME=%2fhome%40%21%23
C_NAME=""
for (( i=0; i<${#NAME}; i++ )); do
CHK=$(echo "{NAME:$i:1}" | grep -v "\%" &> /dev/null;echo $?)
if [[ ${CHK} -eq 0 ]]; then
C_NAME=`echo "$C_NAME${NAME:$i:1}"`
else
HEX=`echo "${NAME:$i:3}" | sed "s/%//"`
C_NAME=`echo -n "$C_NAME";printf "\x$HEX"`
continue 2
fi
done
echo "$C_NAME"
OUTPUT:
/2fhome#40!21#23
EXPECTED:
/home#!#
So basically the conversion is happening, but not in place. Its retaining the hex values as well, which tells me the continue 2 statement is probably not working as I expect in my code. Any workarounds please.
You only have one loop so I assume you expected that continue 2 skips the current and next iteration of the current loop, however, the documentation help continue clearly states
continue [n]
[...]
If N is specified, resumes the Nth enclosing loop.
There is no built-in to skip the current and also the next iteration of the current loop, but in your case you can use (( i += 2 )) instead of continue 2.
Using the structure of your script with some simplifications and corrections:
#!/bin/bash
name=%2fhome%40%21%23
c_name=""
for (( i=0; i<${#name}; i++ )); do
c=${name:i:1}
if [[ $c != % ]]; then
c_name=$c_name$c
else
hex=${name:i+1:2}
printf -v c_name "%s\x$hex" "$c_name"
(( i += 2 )) # stolen from Dudi Boy's answer
fi
done
echo "$c_name"
Always use lower case or mixed case variables to avoid the chance of name collisions with shell or environment variables
Always use $() instead of backticks
Most of the echo commands you use aren't necessary
You can avoid using sed and grep
Variables should never be included in the format string of printf but it can't be avoided easily here (you could use echo -e "\x$hex" instead though)
You can do math inside parameter expansions
% doesn't need to be escaped in your grep command
You could eliminate the $hex variable if you used its value directly:
printf -v c_name "%s\x${name:i+1:2}" "$c_name"
I really enjoyed your exercise and decided to solve it with awk (my current study).
Hope you like it as well.
cat script.awk
BEGIN {RS = "%[[:xdigit:]]+"} { # redefine record separtor to RegEx (gawk specific)
decNum = strtonum("0x"substr(RT, 2)); # remove prefix # from record separator, convert hex num to dec
outputStr = outputStr""$0""sprintf("%c", decNum); # reconstruct output string
}
END {print outputStr}
The output
echo %2fhome%40%21%23 |awk -f script.awk
/home#!#

Loop over first 10 of 100 subdirectories

I have 100 subdirectories and I wanted to loop through the first ten of them in a bash for loop such like that:
for d in ./output/*[0..9];
do
echo $d
done
But the output seems not what I expected:
./output/405050
./output/405140
./output/405309
./output/405310
./output/405319
./output/500550
./output/500589
./output/500610
Why only 8 were printed and my question is how to select a fix number elements from this type of for loop.
*[0..9] loops over ones that end in a 0, 9, or .. If you had written *{0..9} that would loop over ones ending in a digit 0 through 9--closer, but still not right.
Try this loop, which reads the first 10 directory names in a loop. It's kinda obtuse. The primary idea is using while read ... < <(cmd) to read a command's output one line at a time. IFS= and -r are pedantic bits to handle directory names with whitespace and backslashes correctly.
while IFS= read -r dir; do
echo "$dir"
done < <(ls output/*/ | head -10)
Or use this more straightforward version with a counter:
i=0
for dir in output/*/; do
echo "$dir"
((++i < 10)) || break
done
Or this one storing the directories in an array:
dirs=(output/*/)
for dir in "${dirs[#]::10}"; do
echo "$dir"
done
You can make a counter:
#!/bin/bash
i=0;
for d in ./output/*/;
do
echo $d
echo ""
if [[ i == 10 ]]; then
break
fi
i+=1
done
With this you asure to get 10 folders.
I very important to do the last backslash to match only directories.

How to know if file in a loop is the last one?

Example
for FILE in $DIR/*
do
if(<is last File>)
doSomethingSpecial($FILE)
else
doSomethingRegular($FILE)
fi
done
What to call for <is last file> to check if the current file is the last one in the array ?
Is there an easy built-in check without checking the array's length by myself ?
What to call for to check if the current file is the last one in the array ?
For a start, you are not using an array. If you were then it would be easy:
declare -a files
files=($DIR/*)
pos=$(( ${#files[*]} - 1 ))
last=${files[$pos]}
for FILE in "${files[#]}"
do
if [[ $FILE == $last ]]
then
echo "$FILE is the last"
break
else
echo "$FILE"
fi
done
I know of no way to tell that you are processing the last element of a list in a for loop. However you could use an array, iterate over all but the last element, and then process the last element outside the loop:
files=($DIR/*)
for file in "${files[#]::${#files[#]}-1}" ; do
doSomethingRegular "$file"
done
doSomethingSpecial "${files[#]: -1:1}"
The expansion ${files[#]:offset:length} evaluates to all the elements starting at offset (or the beginning if empty) for length elements. ${#files[#]}-1 is the number of elements in the array minus 1.
${files[#]: -1:1} evaluates to the last element - -1 from the end, length 1. The space is necessary as :- is treated differently to : -.
Try this
LAST_FILE=""
for f in *
do
if [ ! -z $LAST_FILE ]
then
echo "Process file normally $LAST_FILE"
fi
LAST_FILE=$f
done
if [ ! -z $LAST_FILE ]
then
echo "Process file as last file $LAST_FILE"
fi
Produces
bash[1051]: ls
1 2 3 4
bash[1052]: sh ../last_file.sh
Process file normally 1
Process file normally 2
Process file normally 3
Process file as last file 4
You can use find to find the total number of files.
Then when you are in the loop count to the total number and carry out your task when the total equals the count i.e, the last file.
f=0
tot_files=`find . -iname '*.txt' | wc -l`
for FILE in $DIR/*
do
f=($f+1)
if [[ $f == $tot_files ]];then
carryout your task
fi
done
Building on the current highest-voted answer from #cdarke (https://stackoverflow.com/a/12298757/415523), if looking at a general array of values (rather than specifically files on disk), the loop code would be as follows:
declare -a array
declare -i length current
array=( a b c d e c )
length=${#array[#]}
current=0
for VALUE in "${array[#]}"; do
current=$((current + 1))
if [[ "$current" -eq "$length" ]]; then
echo "$VALUE is the last"
else
echo "$VALUE"
fi
done
This yields the output:
a
b
c
d
e
c is the last
This ensures that only the last item in the array triggers the alternative action and that, if any other item in the array duplicates the last value, the alternative action is not called for the earlier duplicates.
In the case of an array of paths to files in a specific directory, e.g.
array=( $DIR/* )
...it is probably less of a concern, since individual filenames within the same directory are almost-certainly unique (unless you have a really odd filesystem!)
You can abuse the positional parameters, since they act similarly to an array,
but are a little easier to manipulate. You should either save the old positional
parameters, or execute in a subshell.
# Method 1: use a subshell. Slightly cleaner, but you can't always
# do this (for example, you may need to affect variables in the current
# shell
files=( $DIR/* )
(
set -- "${files[#]}"
until (( $# == 1 )); do
doSomethingRegular "$1"
shift
done
doSomethingSpecial "$1"
)
# Method 2: save the positional parameters. A bit uglier, but
# executes everything in the same shell.
files=( $DIR/* )
oldPP=( "$#" )
set -- "${files[#]}"
until (( $# == 1 )); do
doSomethingRegular "$1"
shift
done
doSomethingSpecial "$1"
set -- "${oldPP[#]}"
What makes a file the last one? Is there something special about it? Is it the file with the greatest name when sorted by name?
Maybe you can take the file names backwards. Then, it's the first file you want to treat special and not the last. figuring out the first is a much easier task than doing the last:
for file in $(ls -r1 $dir)
do
if [ ! $processedLast ]
then
doSomethingSpecial($file)
processedLast=1
else
doSomethingRegular($file)
fi
done
No arrays needed. Actually, I like chepner's answer about using positional parameters.
It's old question - but building on answer from #GregReynolds please use this one-liner if commands differ only by parameters on last pass. Ugly, ugly code for one-liner lovers
( ff="" ; for f in * "" ; do [ -n "$ff" ] && echo $(${f:+false} && echo $ff alternate params here || echo normal params $ff ) ; ff=$f ; done )
normal params 1
normal params 2
normal params 3
4 alternate params here

compound comparisons in bash

can anybody explain why the following bash code involving compound operators is not behaving as expected? basically, nothing enters the if statement inside the for loop but i am passing it correct parameters that should return something by running:
./my_bash_script 20100101 20120101
dates.txt is a list of all days since 2000
#!/bin/bash
old_IFS=$IFS
IFS=$'\n'
lines=($(cat dates.txt)) # array
IFS=$old_IFS
for (( i=1; i<${#lines[#]}; i++ ))
do
if [[ ${line[$i]} -ge $1 && ${line[$i]} -le $2 ]]; then
echo 0 > ${line[$i]} # redirect to file
echo ${line[$i]}
fi
done
The problem is that you've declared an array named lines, but then you try to access it as though it were named line. You need to change every occurrence of ${line[$i]} to ${lines[$i]}.
Better yet, you can dispense with the arithmetic for-loop, and write:
for line in "${lines[#]}" ; do
which will let you refer to the line as $line or "$line" rather than as ${lines[$i]}.
(By the way, how come you have that logic to modify $IFS? It seems like its default value would work just as well.)

Nested loops seem to skip over second loop in Bash

I am trying to write a script that:
a) reads the content of a .csv file
b) sets a variable to the value in the first position (ie to the left of the comma)
c) compare the variable value to each position in an array. If the value is in the array execute one command, if it isn't, insert that value into the first available slot in the array.
The .csv file is in the format:
co:7077,he17208am3200816internet.pdf,he17208am3200917internet.pdf
co:7077,he17208am3200817internet.pdf,he17208am3200918internet.pdf
co:7077,he17208am3200818internet.pdf,he17208am3200919internet.pdf
co:7077,he17208am3200819internet.pdf,he17208am3200915internet.pdf
co:7162,tra210051internet.pdf,tra21005101internet.pdf
co:7162,tra210051appinternet.pdf,tra21005102internet.pdf
co:7178,tra4157l11201021internet.pdf,tra4158l11201021internet.pdf
co:7178,tra4157l11201022internet.pdf,tra4158l11201022internet.pdf
My script so far looks like:
#!/bin/bash
declare -a array
anum=0
src=source.csv
pid=0
while read line;
do
pid=$( echo $line | awk '{print$1}' FS=",")
for n in "${array[#]}";
do
if [[ "$pid" = "$n" ]] ;
then
echo Duplicate value: "$pid";
else
array[$anum]="$pid"
anum=$(( $anum +1 ))
fi
done
done < $src
echo ${array[#]}
When the script is executed the pid is successfully set and reset with each iteration of the while loop, but apparently the nested for loop is never ran.
From my google'ing I suspect it has something to do with the pipe in pid line, but I'll be buggered if I can figure out how to make it work.
Any help is greatly appreciated.
You're not populating your array. The for loop is never executed because the array is empty.
Set a flag in the else clause instead of adding the array element there. After your for loop if the flag is set, add the array element. Don't forget to unset the flag.
You can do array[anum++] without the next line or (( anum++ )) instead of anum=$(($anum + 1)).
Also: while IFS=, read -r pid discard if you don't need the rest of the line (you could do it a little differently if you need it). Doing this, you won't need the echo and awk.
why did you use double square brackets? and also you used a single equals rather than double in the if?
try these one-liners...
$ if [ "a" == "b" ] ; then echo hello ; fi
$ if [ "a" == "a" ] ; then echo hello ; fi

Resources