nested functions and while loops returned `while: command not found` in bash [duplicate] - linux

This question already has answers here:
Why does /bin/sh behave differently to /bin/bash even if one points to the other?
(4 answers)
Closed 7 years ago.
The main function execSteps executes emerge --pretend $package one by one, and these package's names(only names, no version information) are stored in a text file stepFile. Some of packages may have extra need for configuring package.use, package.license, this kind of extra information will be shown up after executed emerge --pretend $package. The second while loop in main function and function acceptPKGTypeItems are intended to deal with this kind of extra information.
When emerging one particular package, it may depend on a couple of more packages. For example, emerge --pretend ceph, I need to emerge more than 10 packages before ceph gets emerged. Along with Gentoo/Linux updated, new version of package may be applied. So text file stepFile only contained with package names which I need, and parsing the result of emerge --pretend $package, I'm able to get updated package emerged.
At case 0), this while loop is intended to parse the result of emerge --pretend $line(which is from stepFile), for example emerge --pretend ceph and get its dependant packages with current version, for example dev-libs/boost-1.57.0, pass it as an argument to function emgRecursion because dependant package dev-libs/boost-1.57.0 of package ceph may have its own dependant packages which are dev-libs/boost-build-1.57.0 and dev-libs/boost-1.57.0.
My problem is I get an error while : command not found in function emgRecursion when I enter 0 at case 0). Is it another different shell thing? I've added a pair parenthesis between the second while loop in main function which helped get readin answer from user for choosing package.use, package.license, or package.keywords. And I've tried to add another pair of parenthesis between the third while loop, the same problem. I've tested emgRecursion and acceptPKGTypeItems separately, both of them work fine and correctly.
Any ideas? Thank you very much.
function acceptPKGTypeItems() {
...
}
function emgRecursion() {
local output="$(emerge --pretend "="$1 | grep "\[ebuild")"
       while read -r line;
       do
done <<<"$output"
}
function execSteps() {
local running=0
while read -r line;
do
if (( running )); then
if [[ $line = "#"* ]] && [[ "${line/"step"}" = "$line" ]]; then
continue
else
if [[ ! "${line/"step"}" = "$line" ]]; then
echo "====== approaching to the next step which is not available at this time."
break
else
( output="$(emerge --pretend $line | grep "\[ebuild")"
echo "**************** $line is ready for emerging ****************"
while read -p "Which type of package would you like to add new item to (1-packageuse 2-packagelicense 3-packagekeywords 0-exit and continue)? " choice; do
case "$choice" in
1) echo "**************** $line is ready for emerging"
acceptPKGTypeItems $PACKAGEUSE
echo "**************** package.use has been updated."
;;
2) echo "**************** $line is ready for emerging"
acceptPKGTypeItems $PACKAGELICENSE
echo "**************** package.license has been updated."
;;
3) echo "**************** $line is ready for emerging"
acceptPKGTypeItems $PACKAGEKEYWORDS
echo "**************** package.keywords has been updated."
;;
0) echo "**************** $line starts emerging"
while read -r element;
do
local str="${element#*"] "}"
str="${str%%" "*}"
echo " $str is an element that need to be emerged. "
emgRecursion "$str"
done <<<"$output"
echo "**************** $line has been emerged. ****************"
break
;;
*) echo "~~~~~~~~~~~~~~~~ Invalid input, try again. ~~~~~~~~~~~~~~~~"
;;
esac
done) </dev/tty
fi
fi
else
[[ $line = "#"$1 ]] && running=1
done <$STEPS
}
execSteps step2
Nothing will stop while loop in main function,
output:
livecd / # ./step1
* Last emerge --sync was 32d 23h 4m 58s ago.
**************** sys-cluster/ceph is ready for emerging ****************
Which type of package would you like to add new item to (1-packageuse 2-packagelicense 3-package.keywords 0-exit and continue)?0
**************** sys-cluster/ceph starts emerging ****************
dev-libs/libaio-0.3.110 is an element that need to be emerged.
* Last emerge --sync was 32d 23h 5m 3s ago.
./step1: line 48:        while: command not found
./step1: line 49:        : command not found
./step1: line 50:                str=dev-libs/libaio-0.3.110: No such file or directory
Take a look at what dev-libs/libaio-0.3.110 looks like.
./step1: line 77:        done: command not found
sys-libs/libunwind-1.1 is an element that need to be emerged.
* Last emerge --sync was 32d 23h 5m 5s ago.
^C
Exiting on signal 2

Problem solved by copying functions other than emgRecursion to another file while I created for testing emgRecursion.
I realized that the difference between these two files(recursion for testing emgRecursion, step for test whole function) is recursion was originally created with #!/bin/bash, and step was originally a plain shell text file without any first line symbol and then I added #!/bin/bash to it. I thought there was no big difference between bash text file and shell text file in terms of syntax. In fact, THEY ARE TOTALLY DIFFERENT. If you mixed them up like my case, it's a WASTE of time.

This would be a template for your while loop. If you want to read a whole line then don't bother with putting a variable in the read line and stop useless Word splitting occurring.
while read -r; do
line=$REPLY
...
done <<<"$OUTPUT"
See Bash-Hackers

Related

linux - simplify semantic versioning script

I have semantic versioning for an app component and this is how I update the major version number and then store it back in version.txt. This seems like a lot of lines for a simple operation. Could someone please help me trim this down? No use of bc as the docker python image I'm on doesn't seem to have that command.
This is extracted from a yml file and version.txt only contains a major and minor number. 1.3 for example. The code below updates only the major number (1) and resets the minor number to 0. So if I ran the code on 1.3, I would get 2.
- echo $(<version.txt) 1 | awk '{print $1 + $2}' > version.txt
VERSION=$(<version.txt)
VERSION=${VERSION%.*}
echo $VERSION > version.txt
echo "New version = $(<version.txt)"
About Simplicity
"Simple" and "short" are not the same thing. echo $foo is shorter than echo "$foo", but it actually does far more things: It splits the value of foo apart on characters in IFS, evaluates each result of that split as a glob expression, and then recombines them.
Similarly, making your code simpler -- as in, limiting the number of steps in the process it goes through -- is not at all the same thing as making it shorter.
Incrementing One Piece, Leaving Others Unmodified
if IFS=. read -r major rest <version.txt || [ -n "$major" ]; then
echo "$((major + 1)).$rest" >"version.txt.$$" && mv "version.txt.$$" version.txt
else
echo "ERROR: Unable to read version number from version.txt" >&2
exit 1
fi
Incrementing Major Version, Discarding Others
if IFS=. read -r major rest <version.txt || [ -n "$major" ]; then
echo "$((major + 1))" >"version.txt.$$" && mv "version.txt.$$" "version.txt"
else
echo "ERROR: Unable to read version number from version.txt" >&2
exit 1
fi
Rationale
Both of the above are POSIX-compliant, and avoid relying on any capabilities not built into the shell.
IFS=. read -r first second third <input reads the first line of input, and splits it on .s into the shell variables first, second and third; notably, the third column in this example includes everything after the first two, so if you had a.b.c.d.e.f, you would get first=a; second=b; third=d.e.f -- hence the name rest to make this clear. See BashFAQ #1 for a detailed explanation.
$(( ... )) creates an arithmetic context in all POSIX-compliant shells. It's only useful for integer math, but since we split the pieces out with the read, we only need integer math. See http://wiki.bash-hackers.org/syntax/arith_expr
Writing to version.txt.$$ and renaming if that write is successful prevents version.txt from being left empty or corrupt if a failure takes place between the open and the write. (A version that was worried about symlink attacks would use mktemp, instead of relying on $$ to generate a unique tempfile name).
Proceeding through to the write only if the read succeeds or [ -n "$major" ] is true prevents the code from resetting the version to 1 (by adding 1 to an empty string, which evaluates in an arithmetic context as 0) if the read fails.

Shell Script working with multiple files [duplicate]

This question already has answers here:
How to iterate over arguments in a Bash script
(9 answers)
Closed 5 years ago.
I have this code below:
#!/bin/bash
filename=$1
file_extension=$( echo $1 | cut -d. -f2 )
directory=${filename%.*}
if [[ -z $filename ]]; then
echo "You forgot to include the file name, like this:"
echo "./convert-pdf.sh my_document.pdf"
else
if [[ $file_extension = 'pdf' ]]; then
[[ ! -d $directory ]] && mkdir $directory
convert $filename -density 300 $directory/page_%04d.jpg
else
echo "ERROR! You must use ONLY PDF files!"
fi
fi
And it is working perfectly well!
I would like to create a script which I can do something like this: ./script.sh *.pdf
How can I do it? Using asterisk.
Thank you for your time!
Firstly realize that the shell will expand *.pdf to a list of arguments. This means that your shell script will never ever see the *. Instead it will get a list of arguments.
You can use a construction like the following:
#!/bin/bash
function convert() {
local filename=$1
# do your thing here
}
if (( $# < 1 )); then
# give your error message about missing arguments
fi
while (( $# > 0 )); do
convert "$1"
shift
done
What this does is first wrap your functionality in a function called convert. Then for the main code it first checks the number of arguments passed to the script, if this is less than 1 (i.e. none) you give the error that a filename should be passed. Then you go into a while loop which is executed as long as there are arguments remaining. The first argument you pass to the convert function which does what your script already does. Then the shift operation is performed, what this does is it throws away the first argument and then shifts all the remaining arguments "left" by one place, that is what was $2 now is $1, what was $3 now is $2, etc. By doing this in the while loop until the argument list is empty you go through all the arguments.
By the way, your initial assignments have a few issues:
you can't assume that the filename has an extension, your code could match a dot in some directory path instead.
your directory assignment seems to be splitting on . instead of /
your directory assignment will contain the filename if no absolute or relative path was given, i.e. only a bare filename
...
I think you should spend a bit more time on robustness
Wrap your code in a loop. That is, instead of:
filename=$1
: code goes here
use:
for filename in "$#"; do
: put your code here
done

Bash script fails to recognise boolean logic, "command not found" [duplicate]

This question already has answers here:
How do I compare two string variables in an 'if' statement in Bash? [duplicate]
(12 answers)
Closed 6 years ago.
I'm new to Bash scripts, trying to make my first backup script. When I run it I always get something like this:
./backupscript.sh: line 9: [3=1: command not found
./backupscript.sh: line 9: 3=2]: command not found
./backupscript.sh: line 15: [3=1: command not found
./backupscript.sh: line 15: 3=3]: command not found
I have tried many different syntax, like ["var"="1"]||["var"=2], double brackets, without quotes, ()-brackets single and double and I'm losing my mind. It seems like bash isn't recognising at all that it's an if-statement. What's wrong? Thanks!
#!/bin/bash
cd /
NOW=$(date +"%m_%d_%Y")
echo "Please input: 1=full 2=system 3=home"
read choice
if ["$choice"="1" || "$choice"="2"]; then
echo "--- STARTING SYSTEMBACKUP ---"
tar -cvpzf systembackup_$NOW.tar.gz --exclude=/proc --exclude=/lost+found --exclude=/systembackup.tar.gz --exclude=/mnt --exclude=/sys --exclude=/dev --exclude=/run --exclude=/media --exclude=/home /
echo "--- SYSTEM BACKUP DONE ---"
fi
if ["$choice"="1" || "$choice"="3"]; then
echo "--- STARTING HOMEBACKUP (excluding ~/Seafile) ---"
tar -cvpzf homebackup_$NOW.tar.gz --exclude=/home/matias/Seafile /home
echo "--- HOMEBACKUP DONE ---"
fi
EDIT: Proper syntax suggested here did the trick, thanks everyone! I'm still looking for good guides on Bash :)
As #fedorqui said, you need spaces around the brackets. That is because [ is actually a synonym for test, a real program, and so is the name of an actual command.
To make your life easier in the future, use spaces and double brackets instead ([[ and ]]). Those are handled internally by bash and are more robust against accidental errors such as forgetting the quotes around a variable substitution. Plus, && and || work as logical AND and OR in double-brackets, but not in single brackets - you have to use -a and -o instead in [ ] expressions.
Using double-brackets ("conditional expressions" in bash), you can write:
if [[ $choice = 1 || $ choice = 3 ]]; then
...
fi
and get the result you expect.
Use the following as a best practice:
if [[ $choice == 1 || $choice == 2 ]]; then
...
fi
Notice the spaces and the == for equality test.
|| and && work in double brackets. Also has some other nice features, like REGEX matching with =~ along with operators like they are known in C-like languages along with less surprises.

"test: too many arguments" message because of special character * while using test command on bash to compare two strings [duplicate]

This question already has answers here:
Meaning of "[: too many arguments" error from if [] (square brackets)
(6 answers)
Closed 5 years ago.
I'm new to shell scripting and I'm having some trouble while using the "test" command and the special character * to compare two strings.
I have to write a shell script which, for every element(both files and directories) contained in the directory passed as the first argument, has to write something(for the solving of my problem it is not relevant to know what has to be written down) on the file "summary.out". What's more, there's a string passed as the second argument. Those files/directories beginning with this string must be ignored(nothing has to be written on summary.out).
Here is the code:
#!/bin/bash
TEMP=NULL
cd "$1"
for i in *
do
if test "$i" != "$2"*;then #Here is where the error comes from
if test -f "$i";then
TEMP="$i - `head -c 10 "$i"`"
elif test -d "$i";then
TEMP="$i - `ls -1 "$i" | wc -l`"
fi
echo $TEMP >> summary.out
fi
done
The error comes from the test which checks whether the current file/directory begins with the string passed as second argument, and it takes place every iteration of the for cycle. It states:"test: too many arguments"
Now, I have performed some tests which showed that the problem has nothing to do with blank spaces inside the $i or $1. The problem is linked to the fact that I use the special character * in the test(if I remove it, everything works fine).
Why can't "test" handle * ? What can I do to fix that?
* gets expanded by the shell.
In bash, you can use [[ ... ]] for conditions instead of test. They support patterns on the right hand side - * is not expanded, as double square brackets are a keyword with higher precedence.
if [[ a == * ]] ; then
echo Matches
else
echo Doesn\'t match
fi

BASH scripting: taking in multiple flags

I have this issue where I am doing a menu sort of program where users are supposed to type in different numbers depending on what operation(s) the want to do.
Right now I need to take in multiple flags to be placed in a command, but I can't seem to get it to work.
Catch: I cannot type in the flags directly into one variable and I will probably have to have this case list to be able to quit at will)
function fileCpy {
printf "Choices:\n1. Interactive copy, answer yes/no before doing the copy: \n2. Make backups of existing destination files\n3. Preserve file attributes\n4. Do a recursive copy\n0. Return to main menu\n"
#read choices
read $a
read $b
read $c
for choices in $choice ; do
case "$choices" in
1)
a=-i ;;
2)
b=--backup ;;
3)
c=-p ;;
#4)
#4= -R -v;;
0)
main;;
esac
echo "$a"
echo "$b"
echo "$c"
printf "\nType the name of the file you wish to copy/backup: "
read $fl1
printf "\nType the destination file: "
read $des1
cp $a $b $c $fl1 $des1
done
}
One problem is that read $a etc is wrong: you should write read a if you need the read at all. As it stands, the value read is stored in the variable with the name stored in $a.
Another problem is that it is far from clear to the innocent user that they're supposed to enter 3 lines of information before the script will continue, but the three read lines force that.
Another problem is that you don't read into $choice (via read choice) so the for loop has nothing to do.
Another problem is that your script will inherit the values of any environment variables that happen to be the same as the names of the variables you're using.
Another problem is that you don't quote the file names. It mostly won't matter unless you have a name that contains spaces or other similarly awkward characters.
A cosmetic issue is that the printf statement is ridiculously long. Use one printf per line. Or use echo. Stuff that scrolls off the RHS of the page is bad (though I don't regard 80 characters as a fixed length for lines, there's a quadratic penalty for lines that are longer than 80 — as (length-80)2 increases, the pain of the longer line goes up.
At another level altogether, the interface is modestly grotesque. As an exercise in shell scripting, it makes sense. As an exercise in how to design good shell scripts, it is a very bad design.
A design that might make sense is:
Set variables to empty: a=""; b=""; c=""; etc.
Offer a range of choices similar to those given now, but add an option to execute the command, and another to abandon ship.
Have a loop that reads choices, and sets flags.
When the user chooses execute, exit the loop and prompt for the file names.
If all's well, execute the command.
Note that you should check that the read commands work; if they don't, fail safe (don't damage anything).
Putting all those together (with some slight differences, but the same overall effect — witness the use of local for the variables):
fileCpy()
{
local a b c file dest
echo "Choices:"
echo "0. Return to main menu"
echo "1. Interactive copy, answer yes/no before doing the copy"
echo "2. Make backups of existing destination files"
echo "3. Preserve file attributes"
echo "4. Do a recursive copy"
echo "5. Execute the copy"
while printf "Your choice: " && read choice
do
[ -z "$choice" ] && return 1 # Empty input - failure
case "$choice" in
(0) return 0;;
(1) a="-i";;
(2) b="--backup";;
(3) c="-p";;
(4) d="-R";;
(5) break;;
(*) echo "Unrecognized response ($choice); please enter 0..5";;
esac
done
[ "$choice" != 5 ] && return 1 # EOF - failure
printf "Type the name of the file you wish to copy/backup: "
read file
[ -z "$file" ] && return 1 # Empty file name - failure
printf "Type the name of the destination file/directory: "
read dest
[ -z "$dest" ] && return 1 # Empty file name - failure
cp $a $b $c "$file" "$dest"
}
Test code:
echo "a=$a b=$b c=$c file=$file dest=$dest"
if fileCpy
then : OK
else echo "Failed"
fi
echo "a=$a b=$b c=$c file=$file dest=$dest"
The last block is a simple test harness. It reports on the values of the variables used inside the function, runs the function, reports if the function failed, and re-echoes the variables to demonstrate that they've not been set.
I would not use that interface unless you paid me to do so, but it more or less meets the goal of a training exercise.
if you want to do menu, you can use select construct (or case/esac). See here for info

Resources