Unix Shell for loop based on an argument - shell

OS = Ubuntu
Filename = generate_zips.sh
I'm trying to create a bunch of files (number based on an arugment). However It is not working.
#!/bin/sh
LIMIT=$1
for ((a=1; a <= LIMIT; a++))
do
mkdir zips
cd zips
touch $a
done
but its returning this
esauer#ubuntu:~/repo/filecheck/src/plugins/archive$ ./generate_zips.sh 10
./generate_zips.sh: 4: ./generate_zips.sh: Syntax error: Bad for loop variable
I cant figure out whats wrong...

Your shebang is wrong - you're using a bash feature (the "C-style" for-loop in double parens) but you specified /bin/sh. Change that to /bin/bash.
You can also change your for-loop so that it doesn't use bash's special features, of course (see the other answers). This is somewhat more portable, as not all Unix-like systems have /bin/bash (but pretty much all of them have /bin/sh). However, bash is pretty wide-spread, too (it's the standard shell in most Linux distributions, for example).

Try:
LIMIT=$1
for a in $(seq 1 $LIMIT)
do
mkdir -p zips
touch zips/$a
done

I might do it something like this...
#!/bin/sh
mkdir zips
cd zips
a=1; while [ $a -le $1 ]; do
touch $a
a=$(($a + 1))
done
There are bash-specific ways that are a bit shorter but this will work on any Posix shell.

Related

This simple bash script doesn't work. What am I doing wrong?

I'm trying to create the script that will simply do cd ../ times n, where n is whatever I pass to it, or 1 by default:
STEPS=${1:-1}
for i in $STEPS; do
cd ..
done
It doesn't give me any errors and it does nothing..
You should be able to source it to do what you wish, e.g.
. yourscript.sh 3
to change directory 3 times. (notice the dot before the yourscript.sh)
After you fix the script at least, e.g.
#!/bin/bash
STEPS=$1
for ((i=1; i<=$STEPS; i++)); do
cd ..
done
Thanks #Charles Duffy for mentioning sourcing, and thanks #chepner for the fixed for loop.
Some info:
Shell scripts are run inside a subshell, and each subshell has its own
concept of what the current directory is. The cd succeeds, but as soon
as the subshell exits, you're back in the interactive shell and
nothing ever changed there.
from here
In bash, you generally don't want to generate a sequence of numbers to iterate over. Use the C-style for loop:
for ((i=1; i<=$STEPS; i++)); do
cd ..
done
If this is in a file, you need to source it ( . ./cd-up) rather than executing it (sh ./cd-up or ./cd-up, etc).
If you are, in fact, using zsh, you can simply use the repeat construct:
repeat $STEPS do cd ..; done
or its shorter form for simple commands
repeat $STEPS cd ..
Assuming that $STEPS is always a single number, then your for loop will only run for one iteration. This is because the type of loop that you're using expects a list of words:
for name in word1 word2 word3; do
# some stuff
done
This will run three times, assigning each value in the list to variable $name.
Using a C-style loop, you could do this:
steps=${1:-1}
for (( i = 0; i < steps; ++i )); do
cd ..
done
Alternatively, in any POSIX shell you can use a while loop:
steps=${1:-1}
while [ $steps -gt 0 ]; do
cd ..
steps=$(( steps - 1 ))
done
If you pass in something that's not an integer, then neither of these approaches will work!
Remember that if you run this as a script, it will change the directory while the script is running but the parent shell (the one you ran the script from) will be unaffected.

bash finding files in directories recursively

I'm studying the bash shell and lately understood i'm not getting right recursive calls involving file searching- i know find is made for this but I'm recently asked to implement a certain search this way or another.
I wrote the next script:
#!/bin/bash
function rec_search {
for file in `ls $1`; do
echo ${1}/${item}
if[[ -d $item ]]; then
rec ${1}/${item}
fi
done
}
rec $1
the script gets as argument file and looking for it recursively.
i find it a poor solution of mine. and have a few improvement questions:
how to find files that contain spaces in their names
can i efficiently use pwd command for printing out absolute address (i tried so, but unsuccessfully)
every other reasonable improvement of the code
Your script currently cannot work:
The function is defined as rec_search, but then it seems you mistakenly call rec
You need to put a space after the "if" in if[[
There are some other serious issues with it too:
for file in `ls $1` goes against the recommendation to "never parse the output of ls", won't work for paths with spaces or other whitespace characters
You should indent the body of if and for to make it easier to read
The script could be fixed like this:
rec() {
for path; do
echo "$path"
if [[ -d "$path" ]]; then
rec "$path"/*
fi
done
}
But it's best to not reinvent the wheel and use the find command instead.
If you are using bash 4 or later (which is likely unless you running this under Mac OS X), you can use the ** operator.
rec () {
shopt -s globstar
for file in "$1"/**/*; do
echo "$file"
done
}

Double parentheses to increment a variable in bash

Suppose I increment a variable in bash. For instance,
> i=0; for f in `ls *.JPG`; do echo $f $i; ((i++)); done
a0.jpg 0
a1.jpg 1
...
Now I wonder why I need those double parentheses to increment i.
The double parentheses construct is a shell feature to support arithmetic operations.
The same construct can also be used for Loops and special numerical constants.
Also, copied from the first link :
# -----------------
# Easter Egg alert!
# -----------------
# Chet Ramey seems to have snuck a bunch of undocumented C-style
#+ constructs into Bash (actually adapted from ksh, pretty much).
# In the Bash docs, Ramey calls (( ... )) shell arithmetic,
#+ but it goes far beyond that.
# Sorry, Chet, the secret is out.
i++ is a perfectly valid file name, and if I have access to your system, I can make that into a command that does something you don't want.
Try creating a file, /bin/i++ with this content:
#!/bin/sh
echo 'Gotcha'
and then chmod +x /bin/i++

Shell Script - Pair numbers with a for statement

I'm trying to make a script that adds pairs of number, using only simple for/if statements, for example:
./pair 1 2 4 8 16
would result in
./pair 1 2 4 8 16 32
3
12
48
My current script is (probably hugely wrong, I'm new to Shell Scripting):
#!/bin/sh
sum=0
count=0
for a in $*
do
sum=`expr $sum + $a`
count=`expr $count + 1`
if [ count=2 ]
then
sum=0
count=0
fi
echo "$sum"
done
exit
However this is not working. Any help would be great.
A for loop isn't really the right tool for this. Use a while loop and the shift command.
while [ $# -gt 1 ]; do
echo $(( $1 + $2 ))
shift 2
done
The problem in your script is that you do not have sufficient whitespace in your if statement, as well as not preceding the variable name with a $:
if [ $count = 2 ]
Also, you only want to output $sum just before you reset its value to 0, not every time through the loop.
And, expr isn't needed for arithmetic anymore: sum=$(( sum + a ))
My approach uses cshell. As a C programmer, I connect better with cshell. But that asside, I approach the problem with an iterator, incrementing twice.
macetw
I haven't seen anyone write in Csh for a long time. Back in the early Unix days, there were two primary shells: Csh and Bourne shell. Bourne was the AT&T flavor and BSD (Berkeley Standard Distribution) used Csh which took syntax hints from the C language.
Most people I knew used Csh as their standard shell, but wrote scripts in Bourne shell because Csh had all sorts of issues as pointed out by Tom Christiansen (as already mentioned by Olivier Dulac). However, Csh had command aliases and command line history and editing. Features that people wanted in their command line shell.
Csh peaked back in the SunOS days. SunOS was based upon BSD and Sun administrators wrote almost all of their scripts in Csh. However, David Korn changed a lot of that with Kornshell. Kornshell contained the standard Bournshell syntax language, but contained features like command aliasing and shell history editing that people wanted in Csh. Not only that, but it contained a lot of new features that were never previously found in shells like built in math (goodbye to expr and bc), pattern matching with [[ ... ]] and shell options that could be set with set -o.
Now, there was little need to know Csh at all. You could use Kornshell as both your scripting language and your command line shell. When Sun replaced SunOS with Solaris, the Kornshell and not the C shell was the default shell. That was the end of C shell as a viable shell choice.
The last big advocate of Csh was Steve Jobs. NeXT used the TurboCsh as its default shell, and it was the default shell on the first iteration of Mac OS X -- long after Sun had abandoned it. However, later versions of Mac OS X defaulted to BASH.
BASH is now the default and standard. As I mentioned before, on most systems, /bin/sh is BASH. In the BASH manpage is this:
If bash is invoked with the name sh, it tries to mimic the startup behavior of historical versions of sh as closely as possible, while conforming to the POSIX standard as well.
That means that solid BASHisms (shopt, ${foo/re/repl}, etc.) are not active, but most of the stuff inherited from Kornshell ([[ ... ]], set -o, typeset, $(( ... )) ) are still available. And, that means about 99% of the BASH scripts will still work even if invoked as /bin/sh.
I believe that most of the criticisms in Tom Christiansen anti-Csh paper no longer apply. Turbo C shell - which long ago replaced the original C shell fixed many of the bugs. However, Turbo C came after much of the world abandoned C shell, so I really can't say.
The Csh approach is interesting, but is not a correct answer to this question. C Shell scripting is a completely different language than Bourne shell. You'd be better off giving an answer in Python.
My approach uses cshell. As a C programmer, I connect better with cshell. But that asside, I approach the problem with an iterator, incrementing twice.
# i = 1
while ( $i < $#argv )
# j = $i + 1
# sum = $argv[$i] + $argv[$j]
echo $sum
# i = $i + 2
end
So the shebangs inplies bourne shell, and if you start the script directly it will be the same as doing:
/bin/sh <your_script_name>
And the simplest loop I can think of would be this one:
while [ $# -gt 1 ]; do
expr $1 + $2
shift 2
done
It counts the input tokens and as long as you have more than 2 (the square brackets test) it adds the first and the second one displaying the result. shift 2 means shifting the input two places (i.e. $1 and $2 are dropped and $n will be mapped to $n-2).
If you want a funny sh/bc combo solution:
#!/bin/sh
printf "%s+%s\n" "$#" | bc
It's cool, printf takes care of the loop itself. :)
Of course, there are no error checkings whatsoever! it will work with floats too.
The same with dc:
#!/bin/sh
printf "%s %s+pc" "$#" | dc
but it won't work with negative numbers. For negative numbers, you could:
#!/bin/sh
printf "%s %s+pc" "${#/-/_}" | dc
And I just realized this is the shortest answer!

Extracting end of filename in bash script

Within my backup script, I'd like to only keep 7 days worth of backups (tried using logrotate for this and it worked perfectly, but I ran into issues with the timing of cron.daily and how it affected the "dateext"). I'm running into problems using parameter expansion to extract the date from the filenames.
Here are some examples of some of the files
foo.bar.tar.gz-20120904
bar.baz.tar.gz-20120904
...
Here is my bash script:
#!/bin/bash
path="/foo/"
today=$(date +%Y%m%d)
keepDays=7
keepSeconds=$(date -d "-$keepDays day" +%s)
for f in $path"*"; do
fileSeconds=$(date -d ${f##*-} +%s)
if [ $fileSeconds -lt $keepSeconds ]
then
rm $f
fi
done
Here is the error I'm getting:
date: extra operand `/foo/foo.bar.tar.gz-20120904'
Remove the quotes around the *, that prevents globbing:
for f in ${path}*; do
(the { } are not strictly required here, but make it easier to read)
Not part of the question, but the Bourne shell syntax [ $fileSeconds -lt $keepSeconds ] could be written as (( $fileSeconds < $keepSeconds )) which is possibly safer.
As cdarke says, remove the quotes around the * in the for loop:
for f in ${path}/*; do
What happens is that the shell executing date gets '/foo/*' and expands that into a list of file names (more than one) and then uses ${f##*-} on part of the list, and date is called with multiple names, and objects.
You'd see this if you ran with bash -x your-script.sh, for instance. When something mysterious goes on, the first step is to make sure you know what the shell is doing. Adding echo "$f" or echo $f in the loop would help you understand — though you'd get two different answers.

Resources