How to fix bash script? - bash

I am interested in automation of Magento installation. I found one nice article with steps, how to achieve this. I downloaded their script and run it from cli:
./mage-install.sh localhost root test "abcABC123" "1.7.0.2"
And get such error:
./mage-install.sh: 21: ./mage-install.sh: Syntax error: "(" unexpected (expecting "}")
I am not familiar with bash, here is a part of code, which is responsible for it:
compareVersions ()
{
typeset IFS='.'
typeset -a v1=( $1 )
typeset -a v2=( $2 )
typeset n diff
for (( n=0; n<4; n+=1 )); do
diff=$((v1[n]-v2[n]))
if [ $diff -ne 0 ] ; then
[ $diff -le 0 ] && echo '-1' || echo '1'
return
fi
done
echo '0'
}
where line 21 is:
typeset -a v1=( $1 )
Can you explain me how to fix it?

The real problem is with the first line of the script:
#!/bin/sh
This specifies what program to use to execute the script when you run it on the command line i.e. ./mage-install.sh. The writers probably developed it on a system where /bin/sh is symlink'd to their bash installation, but that's not the case on many systems (for instance I have Crunchbang which uses dash instead).
You can either explicitly run it with bash using bash mage-install.sh blah blah blah or you can change that first line to point to something that will actually run bash. You can use which bash to see where it lives but it should be in /bin/bash

I should run script like this:
bash mage-install.sh localhost root test "abcABC123" "1.7.0.2"

Related

bash script: how to "exit" from sourced script, and allow to work non sourced?

I have a script that I'd like people to source, but optionally so. So they can run it with or without sourcing it, it's up to them.
e.g. The following should both work:
$ . test.sh
$ test.sh
The problem is, test.sh contains exit statements if correct args aren't passed in. If someone sources the script, then the exit commands exit the terminal!
I've done a bit of research and see from this StackOverflow post that I could detect if it's being sourced, and do something different, but what would that something different be?
The normal way to exit from a sourced script is simply to return (optionally adding the desired exit code) outside of any function. Assuming that when run as a command we have the -e flag set, this will also exit from a shell program:
#!/bin/sh -eu
if [ $# = 0 ]
then
echo "Usage $0 <argument>" >&2
return 1
fi
If we're running without -e, we might be able to return || exit instead.
There may be better ways to do this, but here's a sample script showing how I got this to work:
bparks#home
$ set | grep TESTVAR
bparks#home
$ ./test.sh
Outputs some useful information to the console. Please pass one arg.
bparks#home
$ set | grep TESTVAR
bparks#home
$ . ./test.sh
Outputs some useful information to the console. Please pass one arg.
bparks#home
$ set | grep TESTVAR
bparks#home
$ ./test.sh asdf
export TESTVAR=me
bparks#home
$ set | grep TESTVAR
bparks#home
$ . ./test.sh asdf
bparks#home
$ set | grep TESTVAR
TESTVAR=me
bparks#home
$
test.sh
#!/usr/bin/env bash
# store if we're sourced or not in a variable
(return 0 2>/dev/null) && SOURCED=1 || SOURCED=0
exitIfNotSourced(){
[[ "$SOURCED" != "0" ]] || exit;
}
showHelp(){
IT=$(cat <<EOF
Outputs some useful information to the console. Please pass one arg.
EOF
)
echo "$IT"
}
# Show help if no args supplied - works if sourced or not sourced
if [ -z "$1" ]
then
showHelp
exitIfNotSourced;
return;
fi
# your main script follows
# this sample shows exporting a variable if sourced,
# and outputting this to stdout if not sourced
if [ "$SOURCED" == "1" ]
then
export TESTVAR=me
else
echo "export TESTVAR=me"
fi
Checkout this answer for better description and porper solution.
And here is how it is used in docker-entrypoint.sh in official Mysql image:
# check to see if this file is being run or sourced from another script
_is_sourced() {
# https://unix.stackexchange.com/a/215279
[ "${#FUNCNAME[#]}" -ge 2 ] \
&& [ "${FUNCNAME[0]}" = '_is_sourced' ] \
&& [ "${FUNCNAME[1]}" = 'source' ]
}

writing from a function in a Bash script leaking file descriptors

We have a shell script that is called by cron and runs as root.
This script outputs logging and debug info, and has been failing at one certain point. This point varies based on how much output the script creates (it fails sooner if we enable more debugging output, for example).
However, if the script is called directly, as a user, then it works without a problem.
We have since created a simplified test case which demonstrates the problem.
The script is:
#!/bin/bash
function log_so () {
local msg="$1"
if [ -z "${LOG_FILE}" ] ; then warn_so "It's pointless use log_so() if LOG_FILE variable is undefined!" ; return 1 ; fi
echo -e "${msg}"
echo -e "${msg}" >> ${LOG_FILE}
(
/bin/true
)
}
LOG_FILE="/usr/local/bin/log_bla"
linenum=1
while [[ $linenum -lt 2000 ]] ; do
log_so "short text: $linenum"
let linenum++
done
The highest this has reached is 244 before dying (when called via cron).
Some other searches recommended using a no-op subshell from the function and also calling /bin/true but not only did this not work, the subshell option is not feasible in the main script.
We have also tried changing the file descriptor limit for root, but that did not help, and have tried using both #!/bin/sh and #!/bin/bash for the script.
We are using bash 4.1.5(1)-release on Ubuntu 10.04 LTS.
Any ideas or recommendations for a workaround would be appreciated.
What about opening a fd by hand and cleaning it up afterwards? I don't have a bash 4.1 to test with, but it might help.
LOG_FILE="/usr/local/bin/log_bla"
exec 9<> "$LOG_FILE"
function log_so () {
local msg="$1"
if [ -z "${LOG_FILE}" ] ; then warn_so "It's pointless use log_so() if LOG_FILE variable is undefined!" ; return 1 ; fi
echo -e "${msg}"
echo -e "${msg}" >&9
return 0
}
linenum=1
while [[ $linenum -lt 2000 ]] ; do
log_so "short text: $linenum"
let linenum++
done
exec 9>&-

Get the exit code for a command in Bash and KornShell (ksh)

I want to write code like this:
command="some command"
safeRunCommand $command
safeRunCommand() {
cmnd=$1
$($cmnd)
if [ $? != 0 ]; then
printf "Error when executing command: '$command'"
exit $ERROR_CODE
fi
}
But this code does not work the way I want. Where did I make the mistake?
Below is the fixed code:
#!/bin/ksh
safeRunCommand() {
typeset cmnd="$*"
typeset ret_code
echo cmnd=$cmnd
eval $cmnd
ret_code=$?
if [ $ret_code != 0 ]; then
printf "Error: [%d] when executing command: '$cmnd'" $ret_code
exit $ret_code
fi
}
command="ls -l | grep p"
safeRunCommand "$command"
Now if you look into this code, the few things that I changed are:
use of typeset is not necessary, but it is a good practice. It makes cmnd and ret_code local to safeRunCommand
use of ret_code is not necessary, but it is a good practice to store the return code in some variable (and store it ASAP), so that you can use it later like I did in printf "Error: [%d] when executing command: '$command'" $ret_code
pass the command with quotes surrounding the command like safeRunCommand "$command". If you don’t then cmnd will get only the value ls and not ls -l. And it is even more important if your command contains pipes.
you can use typeset cmnd="$*" instead of typeset cmnd="$1" if you want to keep the spaces. You can try with both depending upon how complex is your command argument.
'eval' is used to evaluate so that a command containing pipes can work fine
Note: Do remember some commands give 1 as the return code even though there isn't any error like grep. If grep found something it will return 0, else 1.
I had tested with KornShell and Bash. And it worked fine. Let me know if you face issues running this.
Try
safeRunCommand() {
"$#"
if [ $? != 0 ]; then
printf "Error when executing command: '$1'"
exit $ERROR_CODE
fi
}
It should be $cmd instead of $($cmd). It works fine with that on my box.
Your script works only for one-word commands, like ls. It will not work for "ls cpp". For this to work, replace cmd="$1"; $cmd with "$#". And, do not run your script as command="some cmd"; safeRun command. Run it as safeRun some cmd.
Also, when you have to debug your Bash scripts, execute with '-x' flag. [bash -x s.sh].
There are several things wrong with your script.
Functions (subroutines) should be declared before attempting to call them. You probably want to return() but not exit() from your subroutine to allow the calling block to test the success or failure of a particular command. That aside, you don't capture 'ERROR_CODE' so that is always zero (undefined).
It's good practice to surround your variable references with curly braces, too. Your code might look like:
#!/bin/sh
command="/bin/date -u" #...Example Only
safeRunCommand() {
cmnd="$#" #...insure whitespace passed and preserved
$cmnd
ERROR_CODE=$? #...so we have it for the command we want
if [ ${ERROR_CODE} != 0 ]; then
printf "Error when executing command: '${command}'\n"
exit ${ERROR_CODE} #...consider 'return()' here
fi
}
safeRunCommand $command
command="cp"
safeRunCommand $command
The normal idea would be to run the command and then use $? to get the exit code. However, sometimes you have multiple cases in which you need to get the exit code. For example, you might need to hide its output, but still return the exit code, or print both the exit code and the output.
ec() { [[ "$1" == "-h" ]] && { shift && eval $* > /dev/null 2>&1; ec=$?; echo $ec; } || eval $*; ec=$?; }
This will give you the option to suppress the output of the command you want the exit code for. When the output is suppressed for the command, the exit code will directly be returned by the function.
I personally like to put this function in my .bashrc file.
Below I demonstrate a few ways in which you can use this:
# In this example, the output for the command will be
# normally displayed, and the exit code will be stored
# in the variable $ec.
$ ec echo test
test
$ echo $ec
0
# In this example, the exit code is output
# and the output of the command passed
# to the `ec` function is suppressed.
$ echo "Exit Code: $(ec -h echo test)"
Exit Code: 0
# In this example, the output of the command
# passed to the `ec` function is suppressed
# and the exit code is stored in `$ec`
$ ec -h echo test
$ echo $ec
0
Solution to your code using this function
#!/bin/bash
if [[ "$(ec -h 'ls -l | grep p')" != "0" ]]; then
echo "Error when executing command: 'grep p' [$ec]"
exit $ec;
fi
You should also note that the exit code you will be seeing will be for the grep command that's being run, as it is the last command being executed. Not the ls.

Possible spacing issue in a bash script. Command will not run in script but will when copied from output

I've gone around and around on the quoting stuff on http://tldp.org for bash and googled until I am blue in the face. I've also tried every obvious quoting scheme for this issue, and yet nothing works.
The problem seems to be that a space inside of a quoted argument in the command run at the end of the script is being interpreted as a separator instead of as a quoted space.
Behold, here's my script (I know full well I'm a noob so comments on my style and/or uneccessary syntax is cool with me, I'll learn):
#!/bin/bash
date=`date`
args="$#"
MSEND_HOME=/home/patrol/Impact #Path to the Impact Directory
integrationName=Introscope #Name of the integration
persistEnabled=1 #1 for Yes, 0 for No
persist=""
bufDir=$MSEND_HOME/tmp/$integrationName #DO NOT CHANGE
cellName=linuxtest #Cell name to forward events to
loggingEnabled=1 #1 for Yes, 0 for No
logFile=$MSEND_HOME/log/$integrationName.$cellName.log
die () {
if [ $loggingEnabled -eq 1 ]
then
echo >>$logFile "$#"
fi
exit 1
}
[ "$#" -ge 1 ] || die "$date - At least 1 argument required, $# provided" "$#"
# This is where you would parse out your arguments and form the following
# slots as a minimum for sending an event.
class=$2
msg=\"$3\"
# Parse the first argument and assign the correct syntax
if [[ $1 == "INFORMATIONAL" ]]
then
severity=INFO
elif [[ $1 == "WARN" ]]
then
severity=WARNING
elif [[ $1 == "CRIT" ]]
then
severity=CRITICAL
else
severity=INFO
fi
#Additional slots can be set, parse them all in this variable;
#e.g., additionalSlots="slot1=value1;slot2=value2;slot3=\"value 3\""
additionalSlots=""
cmd="$MSEND_HOME/bin/msend"
cmd="$cmd -q"
cmd="$cmd -l $MSEND_HOME"
if [ $persistEnabled -eq 1 ]
then
cmd="$cmd -j $bufDir"
fi
cmd="$cmd -n $cellName"
cmd="$cmd -a $class"
cmd="$cmd -m $msg"
cmd="$cmd -r $severity"
if [ $additionalSlots ]
then
cmd="$cmd -b $additionalSlots"
fi
$cmd || die "$date - msend exited with error $? | Original arguments: $args | Command: $cmd"
#echo "msend exited with error $? | Original arguments: $args | Command: $cmd"
The script is executed like this:
./sendEvent.sh "CRIT" "EVENT" "Test Event"
The error I get from the msend executable is that the arguments are wrong, but I'm logging the command line in it's entirety to a file and when I run that logged command in the shell interactively, it works.
Here's the log output:
Tue Oct 4 20:31:29 CDT 2011 - msend exited with error 27 | Original arguments: CRIT EVENT Test Event | Command: /home/patrol/Impact/bin/msend -q -l /home/patrol/Impact -j /home/patrol/Impact/tmp/Introscope -n linuxtest -a EVENT -m "Test Event" -r CRITICAL
So if I paste /home/patrol/Impact/bin/msend -q -l /home/patrol/Impact -j /home/patrol/Impact/tmp/Introscope -n linuxtest -a EVENT -m "Test Event" -r CRITICAL and run it, it works.
If I run the script like ./sendEvent.sh "CRIT" "EVENT" "TestEvent" it works. But I need that argument to allow spaces.
I'm on the track that it's an $IFS issue or something... maybe a difference between the interactive shell and the script environment.
I'd appreciate any insight from smarter people than me!
tl;dr - My command doesn't work when run from within a script, but does when the logged command syntax is used in an interactive shell.
Short answer: see BashFAQ #50.
Long answer: When bash parses a line, it parses quote marks before doing variable substitution; as a result, when you put quotes inside a variable, they don't do what you'd expect. You're actually passing an argument list including '-m' '"Test' 'Event"' '-r' -- those double-quotes aren't around the arguments, they're in the arguments.
In this case, the best solution is to build the command in an array rather than a string. Also, get in the habbit of putting double-quotes around variables (e.g. filenames) when you use them, to prevent confusion if they contain spaces. With those changes (and a few other tweaks), here's my version of your script:
#!/bin/bash
date="$(date)" # Backquotes are confusing, use $() instead
args=("$#") # Save the args in an array rather than mushing them together in a string
MSEND_HOME=/home/patrol/Impact #Path to the Impact Directory
MSEND_HOME="$HOME/tmp" #Path to the Impact Directory
integrationName=Introscope #Name of the integration
persistEnabled=1 #1 for Yes, 0 for No
persist=""
bufDir="$MSEND_HOME/tmp/$integrationName" #DO NOT CHANGE
cellName=linuxtest #Cell name to forward events to
loggingEnabled=1 #1 for Yes, 0 for No
logFile="$MSEND_HOME/log/$integrationName.$cellName.log"
die () {
if [ $loggingEnabled -eq 1 ]
then
echo >>"$logFile" "$#"
fi
exit 1
}
[ "$#" -ge 1 ] || die "$date - At least 1 argument required, $# provided" "$#"
# This is where you would parse out your arguments and form the following
# slots as a minimum for sending an event.
class="$2" # Quotes not strictly needed here, but a good habbit
msg="$3"
# Parse the first argument and assign the correct syntax
if [[ "$1" == "INFORMATIONAL" ]]
then
severity=INFO
elif [[ "$1" == "WARN" ]]
then
severity=WARNING
elif [[ "$1" == "CRIT" ]]
then
severity=CRITICAL
else
severity=INFO
fi
#Additional slots can be set, parse them all in this array;
#e.g., additionalSlots="slot1=value1;slot2=value2;slot3=value 3" # Don't embed quotes
additionalSlots=""
cmd=("$MSEND_HOME/bin/msend") # Build the command as an array, not a string
cmd+=(-q) # Could equivalently use cmd=("${cmd[#]}" -q), but this is simpler
cmd+=(-l "$MSEND_HOME")
if [ $persistEnabled -eq 1 ]
then
cmd+=(-j "$bufDir")
fi
cmd+=(-n "$cellName")
cmd+=(-a "$class") # Possible bug: $2 and #3 aren't required, but they're getting added unconditionally
cmd+=(-m "$msg") # These should probably be conditional, like additionalSlots
cmd+=(-r "$severity")
if [ -n "$additionalSlots" ]
then
cmd+=(-b "$additionalSlots")
fi
"${cmd[#]}" || die "$date - msend exited with error $? | Original arguments:$(printf " %q" "${args[#]}") | Command:$(printf " %q" "${cmd[#]}")"
#echo "msend exited with error $? | Original arguments:$(printf " %q" "${args[#]}") | Command:$(printf " %q" "${cmd[#]}")"
I think the arg goes wrong with this assignment: cmd="$cmd -m $msg".
Change it to cmd="$cmd -m \"$msg\"".
Okay, I don't see the exact problem immediately, but I can tell you what it is; this hint should help.
Remember that the shell quoting mechanism only interprets a string once. As a result, if you're not careful, what you thought was "foo" "a" "b" is in fact "foo a b" -- that is, all one token, not three.
Run the script with bash -x which will show you at each step what the shell is actually seeing.

Bash programming with filesystem functions

I have been busy this week trying to wrap my head around a little Bash program to migrate a CMS from one server to another. The reasopn for this is because I have more tha 40 of these to do, and need to get it done in a timely manner, thus the Bash idea.
Needless to say, I have run into a couple of problems so far, but one of them has halted my development completetly, directory checking.
No I have tried a couple of methods and none of them seem to work really. The catch is that I have to check the folder on a remote server via ssh. Here my example:
ExSshRsa=~/.ssh/id_rsa
ExSshPort=22
ExSshHost=localhost
ExRoot=/var/www/
echo -n "Verifying Root access $ExRoot..."
SSHRoot='ssh -i $ExSshRsa -p $ExSshPort $ExSshHost [ -d $ExRoot ] || exit 1 '
echo $SSHRoot
if [ "$SSHRoot" -eq 0 ]
then
echo "OK"
else
echo "FAIL"
fi
I get the Error: [: : integer expression expected
Does the [ or test not resturn a 0 which is numerical. ?
Passing strings as arguments to a remote host is not trivial; you need to use arrays. A test example:
declare -a cmd=(touch "file name with spaces")
printf -v escaped_cmd_str '%q ' "${cmd[#]}"
ssh localhost $escaped_cmd
ssh localhost ls # Should return "file name with spaces" on a separate line
So your case should be:
ExSshRsa=~/.ssh/id_rsa
ExSshPort=22
ExSshHost=localhost
ExRoot=/var/www/
echo -n "Verifying Root access $ExRoot..."
declare -a cmd=( '[' -d "$ExRoot" ']' ) # Need to quote "[" since it's a Bash-specific symbol
printf -v escaped_cmd_str '%q ' "${cmd[#]}"
if ssh -i "$ExSshRsa" -p "$ExSshPort" "$ExSshHost" $escaped_cmd
then
echo "OK"
else
echo "FAIL"
fi
This is a rare case where using unquoted variable expansion is perfectly fine.
change the shebang to #!/bin/bash -x and look at the output...
you are storing a string in variable SSHRoot using single quotes, meaning that no variables will be expanded, i.e. a $ is still a $. Use double quotes instead, i.e. "
to store the output from a command in bash, use
var=$(cmd)
the exist status of a command is stored in the variable $?. Do a check on that after the ssh-command
you are never executing the ssh-command in your code
Great link here for bash-programming
Try the following:
ExSshRsa=~/.ssh/id_rsa
ExSshPort=22
ExSshHost=localhost
ExRoot=/var/www/
echo -n "Verifying Root access $ExRoot..."
cmd="bash -c \"[ -d $ExRoot ] || exit 1\""
SSHRoot="ssh -i $ExSshRsa -p $ExSshPort $ExSshHost ${cmd}"
$SSHRoot
if [ $? -eq 0 ]
then
echo "OK"
else
echo "FAIL"
fi
The variables weren't being replaced in your SSHRoot variable as it's in single quotes. Also, you weren't passing an executable command, so that's why I use bash -c above. It will run the bash commands inside the quoted string.
$? stores the exit value of the last command, in this case the SSHRoot one.
#!/bin/bash
ExSshRsa=~/.ssh/id_rsa
ExSshPort=22
ExSshHost=localhost
ExBase='/tmp/'
ExRoot='one space/'
declare -a AExRoot
for argR in "${ExRoot[#]}"
do
ExRoot+=($(printf %q "$argR"))
done
clear
FRoot=( $ExBase${ExRoot[#]} )
echo -n "Verifying Root access $FRoot..."
SSHRootTest="bash -c \"[ -d $FRoot ] && echo 0 && exit 0 || echo 1 && exit 1\""
SSHRoot=$( ssh -i $ExSshRsa -p $ExSshPort $ExSshHost ${SSHRootTest})
if [ $? -eq 0 ]
then
echo -en "\e[1;32mOK\e[0;37;m..."
else
echo -en "\e[1;31mFAIL\e[0;37;m..."
fi
sleep 1
if [ -w $FRoot ]
then
echo -e "\e[1;32mwritable\e[0;37;m"
else
echo -e "\e[1;31mNOT writeable\e[0;37;m"
fi
echo -e "\e[0;m"
exit 0
So I have incorporated all of the suggestions so far and have one last problem, the FRoot is not getting populated by the complete array values. Other than that I think it now has the subjective approach as suggested #john-keyes, the proper expansion #frederik and the crazy space escapes #l0b0

Resources