Assignment (str=$*) trying to run as a command in bash - shell

input in terminal:
cd Desktop
chmod 777 isPalindromeFun.sh
./isPalindromeFun.sh
isPalindrome madam
gives an error: str=madam not found
#!/bin/bash
function isPalindrome () {
if [ "$#" -ne 0 ];
then
inc=$1; # if it has arguments set the increment with the first argument
fi
[ $# -eq 0 ] && { read str ;} || \ str=$*
String="$(echo $str | sed 's/[^[:alnum:]]//g' | \
tr '[:upper:]' '[:lower:]')"
if [ "$(echo $String | rev)" = "$String" ]
then
echo "\"$str\" is a palindrome"
else
echo "\"$str\" is not a palindrome"
fi
}

The backslash needs to be removed when writing this as a single line:
[ "$#" -eq 0 ] && { read str ;} || \ str=$*
It would make more sense if writing the code as two lines:
[ "$#" -eq 0 ] && { read str ;} || \
str=$*
On just one line, however, the backslash only has the effect of escaping the character following it -- making that next character, a space, be data rather than syntax -- and thus makes your command
str=$*
instead be the same as
" str="$*
meaning the space is part of the command. That's not a valid assignment, and of course there's no command called " str=madam" (starting with a space!) on your system.
Aside: $# doesn't strictly need to be quoted -- unless you have IFS set to a value that contains numbers, the results from expanding $# are guaranteed to expand to a single word -- but I'm doing so here as a workaround for StackOverflow's syntax highlighting.

Related

how to handle filename that has special characters (hypen) in bash scripting? [duplicate]

This question already has answers here:
When to wrap quotes around a shell variable?
(5 answers)
Closed 5 months ago.
I have a bash function that crawls through the current directory of the bash script and find for files that have a certain file extensions. Everything is working fine until I have files that have bash-related special characters like '-' in the filename.
My question is, how do I handle the dashes in the filename? Thank you in advance!
Directory
./1a.log
./1b.log
./1c.log
./1d file.log
./1e file_working.log
./1f-notworking.log #error
logparser.sh
read_files() {
files=()
file_ext="${FILE##*.}"
if [ -f "$FILE" ] && [[ $file_ext == log ]]; then
msg "${RED}Parsing file: ${CYAN}$FILE"
files+=($FILE)
elif [ -d "$FILE" ]; then
msg "${RED}Parsing file: ${BLUE}$FILE"
for FILENAME in "$FILE"/*; do
dir_ext="${FILENAME##*.}"
if [ -f $FILENAME ] && [[ $dir_ext == log ]]; then
files+=($FILENAME)
fi
done
else
msg "${RED}Unable to process: ${CYAN}$FILE .Skipping"
fi
}
Tracestack
[: syntax error: `-' unexpected
After reading the post recommended by #GordonDavisson , the issue that I have faced is due to not quoting the variable that is storing my filename.
This is because "parameter expansions are subjected to both word-splitting and pathname expansion" [https://stackoverflow.com/questions/55023461/when-should-i-double-quote-a-parameter-expansion]. By quoting, it preserves the literal content of the parameter.
"General rule: quote it if it can either be empty or contain spaces (or any whitespace really) or special characters (wildcards). Not quoting strings with spaces often leads to the shell breaking apart a single argument into many" [https://stackoverflow.com/a/10067297/8638278]
logparser.log
read_files() {
files=()
file_ext="${FILE##*.}"
if [ -f "$FILE" ] && [[ $file_ext == log ]]; then
msg "${RED}Parsing file: ${CYAN}$FILE"
files+=("$FILE")
elif [ -d "$FILE" ]; then
msg "${RED}Parsing file: ${BLUE}$FILE"
for FILENAME in "$FILE"/*; do
dir_ext="${FILENAME##*.}"
if [ -f "$FILENAME" ] && [[ $dir_ext == log ]]; then
files+=("$FILENAME")
fi
done
else
msg "${RED}Unable to process: ${CYAN}$FILE .Skipping"
fi
}
#!/bin/sh -x
find . | sed -n '/*.log/p' > stack
cat > ed1 <<EOF
1p
q
EOF
cat > ed2 <<EOF
1d
wq
EOF
next () {
[[ -s stack ]] && main
exit 0
}
main () {
line=$(ed -s stack < ed1)
msg "${RED}Parsing file: ${CYAN}$line"
ed -s stack < ed2
next
}
next
This exclusively uses files with a log extension, so you don't need to check.

bash assign a value to variable if environment variable is set in a single line

How do set a variable value to something based on an environment variable is set.
For example, I want it to be
var1 = var2 if ENV_VARIABLE==true else var3
(using python syntax for simplicity)
I want this to happen in a single line as well. What is the correct syntax in this case in bash. The guide at https://unix.stackexchange.com/questions/122845/using-a-b-for-variable-assignment-in-scripts is good but does not cover this case.
This is portable to ur-sh and technically possible to put on a single line.
case $ENV_VARIABLE in true) var1=var2;; *) var1=var3;; esac
Try this:
[[ "$ENV_VARIABLE" == "true" ]] && var1="$var2" || var1="$var3"
var1 = var2 if ENV_VARIABLE==true else var3
So:
var2=smth
var3="other smth"
ENV_VARIABLE=true
we can (the best, does not remove trailing newlines):
if [ "$ENV_VARIABLE" = true ]; then var1=$var2; else var1=$var3; fi
or (assuming ENV_VARIABLE is a string "true" or a string "false" only):
if "$ENV_VARIABLE"; then var1=$var2; else var1=$var3; fi
or (removes trailing newlines)
var1=$(if "$ENV_VARIABLE"; then echo "$var2"; else echo "$var3"; fi)
or (removes trailing newlines)
var1=$("$ENV_VARIABLE" && echo "$var2" || echo "$var3")
or (removes trailing newlines and eval is evil)
var1=$(eval echo "\$$("$ENV_VARIABLE" && echo var2 || echo var3)")
or (eval is evil)
eval var1=\$$(arr[0]=var2; arr[1]=var3; [ "$ENV_VARIABLE" = true ]; echo "${arr[$?]}")
or (eval is evil)
eval var1=\$$("$ENV_VARIABLE" && echo var2 || echo var3)
or (removes trailing newlines)
printf -v var1 "%s" "$("$ENV_VARIABLE" && echo "$var2" || echo "$var3")"
or (crazy)
var1=$("$ENV_VARIABLE"; printf "%s\x00" "$var2" "$var3" | head -z -n$(($?+1)) | tail -z -n1 | tr -d '\000')
or (named reference variable, so not really assignment, more a pointer)
declare -n var1=$("$ENV_VARIABLE" && echo "var2" || echo "var3")
Puff, out of ideas. I honesty think that only the first one is the proper one. I sometimes in my scripts use booleans variables as true and false strings, which allows for removing only the [ ... = true ] parts.
if statement can be written on a single line.
-z can be used to test if a variable is defined and has a value.
-a in the test, stands for AND
== EDIT ==
(see remark from #tripleee: the first version contains a -a which is not reliable. I did not found an issue with the first version proposed, but it seems to be a good practice to avoid the use -a and -o)
Give a try to this:
if [[ ! -z "${ENV_VARIABLE}" && "${ENV_VARIABLE}" = true ]] ; then var1="${var2}"; else var1="${var3}"; fi
.
.
== FIRST VERSION ==
if [ ! -z "${ENV_VARIABLE}" -a "${ENV_VARIABLE}" = true ] ; then var1="${var2}"; else var1="${var3}"; fi
Use default and alternate values together.
var1=${ENV_VARIABLE:+$var2} var1=${var1:-$var3}
$ var2=foo
$ var3=bar
$ var1=${ENV_VARIABLE:+$var2} var1=${var1:-$var3}
$ echo $var1
bar
$
$ var1=${HOME:+$var2} var1=${var1:-$var3}
$ echo $var1
foo

Iterate over a list of quoted strings

I'm trying to run a for loop over a list of strings where some of them are quoted and others are not like so:
STRING='foo "bar_no_space" "baz with space"'
for item in $STRING; do
echo "$item"
done
Expected result:
foo
bar_no_space
baz with space
Actual result:
foo
"bar_no_space"
"baz
with
space"
I can achieve the expected result by running the following command:
bash -c 'for item in '"$STRING"'; do echo "$item"; done;'
I would like to do this without spawning a new bash process or using eval because I do not want to take the risk of having random commands executed.
Please note that I do not control the definition of the STRING variable, I receive it through an environment variable. So I can't write something like:
array=(foo "bar_no_space" "baz with space")
for item in "${array[#]}"; do
echo "$item"
done
If it helps, what I am actually trying to do is split the string as a list of arguments that I can pass to another command.
I have:
STRING='foo "bar_no_space" "baz with space"'
And I want to run:
my-command --arg foo --arg "bar_no_space" --arg "baz with space"
Use an array instead of a normal variable.
arr=(foo "bar_no_space" "baz with space")
To print the values:
print '%s\n' "${arr[#]}"
And to call your command:
my-command --arg "${arr[0]}" --arg "${arr[1]}" --arg "{$arr[2]}"
Can you try something like this:
sh-4.4$ echo $string
foo "bar_no_space" "baz with space"
sh-4.4$ echo $string|awk 'BEGIN{FS="\""}{for(i=1;i<NF;i++)print $i}'|sed '/^ $/d'
foo
bar_no_space
baz with space
Solved: xargs + subshell
A few years late to the party, but...
Malicious Input:
SSH_ORIGINAL_COMMAND='echo "hello world" foo '"'"'bar'"'"'; sudo ls -lah /; say -v Ting-Ting "evil cackle"'
Note: I originally had an rm -rf in there, but then I realized that would be a recipe for disaster when testing variations of the script.
Converted perfectly into safe args:
# DO NOT put IFS= on its own line
IFS=$'\r\n' GLOBIGNORE='*' args=($(echo "$SSH_ORIGINAL_COMMAND" \
| xargs bash -c 'for arg in "$#"; do echo "$arg"; done'))
echo "${args[#]}"
See that you can indeed pass these arguments just like $#:
for arg in "${args[#]}"
do
echo "$arg"
done
Output:
hello world
foo
bar;
sudo
rm
-rf
/;
say
-v
Ting-Ting
evil cackle
I'm too embarrassed to say how much time I spent researching this to figure it out, but once you get the itch... y'know?
Defeating xargs
It is possible to fool xargs by providing escaped quotes:
SSH_ORIGINAL_COMMAND='\"hello world\"'
This can make a literal quote part of the output:
"hello
world"
Or it can cause an error:
SSH_ORIGINAL_COMMAND='\"hello world"'
xargs: unmatched double quote; by default quotes are special to xargs unless you use the -0 option
In either case, it doesn't enable arbitrary execution of code - the parameters are still escaped.
Pure bash parser
Here's a quoted-string parser written in pure bash (what terrible fun)!
Caveat: just like the xargs example above, this errors in the case of an escaped quoted.
Usage
MY_ARGS="foo 'bar baz' qux * "'$(dangerous)'" sudo ls -lah"
# Create array from multi-line string
IFS=$'\r\n' GLOBIGNORE='*' args=($(parseargs "$MY_ARGS"))
# Show each of the arguments array
for arg in "${args[#]}"; do
echo "$arg"
done
Output:
$#: foo bar baz qux *
foo
bar baz
qux
*
Parse Argument Function
Literally going character-by-character and adding to the current string, or adding to the array.
set -u
set -e
# ParseArgs will parse a string that contains quoted strings the same as bash does
# (same as most other *nix shells do). This is secure in the sense that it doesn't do any
# executing or interpreting. However, it also doesn't do any escaping, so you shouldn't pass
# these strings to shells without escaping them.
parseargs() {
notquote="-"
str=$1
declare -a args=()
s=""
# Strip leading space, then trailing space, then end with space.
str="${str## }"
str="${str%% }"
str+=" "
last_quote="${notquote}"
is_space=""
n=$(( ${#str} - 1 ))
for ((i=0;i<=$n;i+=1)); do
c="${str:$i:1}"
# If we're ending a quote, break out and skip this character
if [ "$c" == "$last_quote" ]; then
last_quote=$notquote
continue
fi
# If we're in a quote, count this character
if [ "$last_quote" != "$notquote" ]; then
s+=$c
continue
fi
# If we encounter a quote, enter it and skip this character
if [ "$c" == "'" ] || [ "$c" == '"' ]; then
is_space=""
last_quote=$c
continue
fi
# If it's a space, store the string
re="[[:space:]]+" # must be used as a var, not a literal
if [[ $c =~ $re ]]; then
if [ "0" == "$i" ] || [ -n "$is_space" ]; then
echo continue $i $is_space
continue
fi
is_space="true"
args+=("$s")
s=""
continue
fi
is_space=""
s+="$c"
done
if [ "$last_quote" != "$notquote" ]; then
>&2 echo "error: quote not terminated"
return 1
fi
for arg in "${args[#]}"; do
echo "$arg"
done
return 0
}
I may or may not keep this updated at:
https://git.coolaj86.com/coolaj86/git-scripts/src/branch/master/git-proxy
Seems like a rather stupid thing to do... but I had the itch... oh well.
Here is a way without an array of strings or other difficulties (but with bash calling and eval):
STRING='foo "bar_no_space" "baz with space"'
eval "bash -c 'while [ -n \"\$1\" ]; do echo \$1; shift; done' -- $STRING"
Output:
foo
bar_no_space
baz with space
If You want to do with the strings something more difficult then just echo You can split Your script:
split_qstrings.sh
#!/bin/bash
while [ -n "$1" ]
do
echo "$1"
shift
done
Another part with more difficult processing (capitalizing of a characters for example):
STRING='foo "bar_no_space" "baz with space"'
eval "split_qstrings.sh $STRING" | while read line
do
echo "$line" | sed 's/a/A/g'
done
Output:
foo
bAr_no_spAce
bAz with spAce

Unexpected end of file bash script

This is just a simple problem but I don't understand why I got an error here. This is just a for loop inside an if statement.
This is my code:
#!/bin/bash
if (!( -f $argv[1])) then
echo "Argv must be text file";
else if ($#argv != 1) then
echo "Max argument is 1";
else if (-f $argv[1]) then
for i in `cut -d ',' -f2 $argv[1]`
do
ping -c 3 $i;
echo "finish pinging host $i"
done
fi
Error is in line 16, which is the line after fi, that is a blank line .....
Can someone please explain why i have this error ????
many, many errors.
If I try to stay close to your example code:
#!/bin/sh
if [ ! -f "${1}" ]
then
echo "Argv must be text file";
else if [ "${#}" -ne 1 ]
then
echo "Max argument is 1";
else if [ -f "${1}" ]
then
for i in $(cat "${1}" | cut -d',' -f2 )
do
ping -c 3 "${i}";
echo "finish pinging host ${i}"
done
fi
fi
fi
another way, exiting each time the condition is not met :
#!/bin/sh
[ "${#}" -ne 1 ] && { echo "There should be 1 (and only 1) argument" ; exit 1 ; }
[ ! -f "${1}" ] && { echo "Argv must be a file." ; exit 1 ; }
[ -f "${1}" ] && {
for i in $(cat "${1}" | cut -d',' -f2 )
do
ping -c 3 "${i}";
echo "finish pinging host ${i}"
done
}
#!/usr/local/bin/bash -x
if [ ! -f "${1}" ]
then
echo "Argument must be a text file."
else
while-loop-script "${1}"
fi
I have broken this up, because I personally consider it extremely bad form to nest one function inside another; or truthfully to even have more than one function in the same file. I don't care about file size, either; I've got several scripts which are 300-500 bytes long. I'm learning FORTH; fractalism in that sense is a virtue.
# while-loop-script
while read line
do
IFS="#"
ping -c 3 "${line}"
IFS=" "
done < "${1}"
Don't use cat in order to feed individual file lines to a script; it will always fail, and bash will try and execute the output as a literal command. I thought that sed printing would work, and it often does, but for some reason it very often substitutes spaces for newlines, which is extremely annoying as well.
The only absolutely bulletproof method of feeding a line to a script that I know of, which will preserve all space and formatting, is to use while-read loops, rather than substituted for cat or for sed loops, as mentioned.
Something else which you will need to do, in order to be sure about preserving whitespace, is to set the internal field seperator (IFS) to something that you know your file will not contain, and then resetting it back to whitespace at the end of the loop.
For every opening if, you must have a corresponding closing fi. This is also true for else if. Better use elif instead
if test ! -f "$1"; then
echo "Argv must be text file";
elif test $# != 1; then
echo "Max argument is 1";
elif test -f "$1"; then
for i in `cut -d ',' -f2 "$1"`
do
ping -c 3 $i;
echo "finish pinging host $i"
done
fi
There's also no argv variable. If you want to access the command line arguments, you must use $1, $2, ...
Next point is $#argv, this evaluates to $# (number of command line args) and argv. This looks a lot like perl.
Furthermore, testing is done with either test ... or [ ... ], not ( ... )
And finally, you should enclose at least your command line arguments in double quotes "$1". If you don't and there is no command line argument, you have for example
test ! -f
instead of
test ! -f ""
This lets the test fail and go on to the second if, instead of echoing the proper message.

Meaning of "[: too many arguments" error from if [] (square brackets)

I couldn't find any one simple straightforward resource spelling out the meaning of and fix for the following BASH shell error, so I'm posting what I found after researching it.
The error:
-bash: [: too many arguments
Google-friendly version: bash open square bracket colon too many arguments.
Context: an if condition in single square brackets with a simple comparison operator like equals, greater than etc, for example:
VARIABLE=$(/some/command);
if [ $VARIABLE == 0 ]; then
# some action
fi
If your $VARIABLE is a string containing spaces or other special characters, and single square brackets are used (which is a shortcut for the test command), then the string may be split out into multiple words. Each of these is treated as a separate argument.
So that one variable is split out into many arguments:
VARIABLE=$(/some/command);
# returns "hello world"
if [ $VARIABLE == 0 ]; then
# fails as if you wrote:
# if [ hello world == 0 ]
fi
The same will be true for any function call that puts down a string containing spaces or other special characters.
Easy fix
Wrap the variable output in double quotes, forcing it to stay as one string (therefore one argument). For example,
VARIABLE=$(/some/command);
if [ "$VARIABLE" == 0 ]; then
# some action
fi
Simple as that. But skip to "Also beware..." below if you also can't guarantee your variable won't be an empty string, or a string that contains nothing but whitespace.
Or, an alternate fix is to use double square brackets (which is a shortcut for the new test command).
This exists only in bash (and apparently korn and zsh) however, and so may not be compatible with default shells called by /bin/sh etc.
This means on some systems, it might work from the console but not when called elsewhere, like from cron, depending on how everything is configured.
It would look like this:
VARIABLE=$(/some/command);
if [[ $VARIABLE == 0 ]]; then
# some action
fi
If your command contains double square brackets like this and you get errors in logs but it works from the console, try swapping out the [[ for an alternative suggested here, or, ensure that whatever runs your script uses a shell that supports [[ aka new test.
Also beware of the [: unary operator expected error
If you're seeing the "too many arguments" error, chances are you're getting a string from a function with unpredictable output. If it's also possible to get an empty string (or all whitespace string), this would be treated as zero arguments even with the above "quick fix", and would fail with [: unary operator expected
It's the same 'gotcha' if you're used to other languages - you don't expect the contents of a variable to be effectively printed into the code like this before it is evaluated.
Here's an example that prevents both the [: too many arguments and the [: unary operator expected errors: replacing the output with a default value if it is empty (in this example, 0), with double quotes wrapped around the whole thing:
VARIABLE=$(/some/command);
if [ "${VARIABLE:-0}" == 0 ]; then
# some action
fi
(here, the action will happen if $VARIABLE is 0, or empty. Naturally, you should change the 0 (the default value) to a different default value if different behaviour is wanted)
Final note: Since [ is a shortcut for test, all the above is also true for the error test: too many arguments (and also test: unary operator expected)
Just bumped into this post, by getting the same error, trying to test if two variables are both empty (or non-empty). That turns out to be a compound comparison - 7.3. Other Comparison Operators - Advanced Bash-Scripting Guide; and I thought I should note the following:
I used -e thinking it means "empty" at first; but that means "file exists" - use -z for testing empty variable (string)
String variables need to be quoted
For compound logical AND comparison, either:
use two tests and && them: [ ... ] && [ ... ]
or use the -a operator in a single test: [ ... -a ... ]
Here is a working command (searching through all txt files in a directory, and dumping those that grep finds contain both of two words):
find /usr/share/doc -name '*.txt' | while read file; do \
a1=$(grep -H "description" $file); \
a2=$(grep -H "changes" $file); \
[ ! -z "$a1" -a ! -z "$a2" ] && echo -e "$a1 \n $a2" ; \
done
Edit 12 Aug 2013: related problem note:
Note that when checking string equality with classic test (single square bracket [), you MUST have a space between the "is equal" operator, which in this case is a single "equals" = sign (although two equals' signs == seem to be accepted as equality operator too). Thus, this fails (silently):
$ if [ "1"=="" ] ; then echo A; else echo B; fi
A
$ if [ "1"="" ] ; then echo A; else echo B; fi
A
$ if [ "1"="" ] && [ "1"="1" ] ; then echo A; else echo B; fi
A
$ if [ "1"=="" ] && [ "1"=="1" ] ; then echo A; else echo B; fi
A
... but add the space - and all looks good:
$ if [ "1" = "" ] ; then echo A; else echo B; fi
B
$ if [ "1" == "" ] ; then echo A; else echo B; fi
B
$ if [ "1" = "" -a "1" = "1" ] ; then echo A; else echo B; fi
B
$ if [ "1" == "" -a "1" == "1" ] ; then echo A; else echo B; fi
B
Another scenario that you can get the [: too many arguments or [: a: binary operator expected errors is if you try to test for all arguments "$#"
if [ -z "$#" ]
then
echo "Argument required."
fi
It works correctly if you call foo.sh or foo.sh arg1. But if you pass multiple args like foo.sh arg1 arg2, you will get errors. This is because it's being expanded to [ -z arg1 arg2 ], which is not a valid syntax.
The correct way to check for existence of arguments is [ "$#" -eq 0 ]. ($# is the number of arguments).
I also faced same problem. #sdaau answer helped me in logical way. Here what I was doing which seems syntactically correct to me but getting too many arguments error.
Wrong Syntax:
if [ $Name != '' ] && [ $age != '' ] && [ $sex != '' ] && [ $birthyear != '' ] && [ $gender != '' ]
then
echo "$Name"
echo "$age"
echo "$sex"
echo "$birthyear"
echo "$gender"
else
echo "Enter all the values"
fi
in above if statement, if I pass the values of variable as mentioned below then also I was getting syntax error
export "Name"="John"
export "age"="31"
export "birthyear"="1990"
export "gender"="M"
With below syntax I am getting expected output.
Correct syntax:
if [ "$Name" != "" -a "$age" != "" -a "$sex" != "" -a "$birthyear" != "" -a "$gender" != "" ]
then
echo "$Name"
echo "$age"
echo "$sex"
echo "$birthyear"
echo "$gender"
else
echo "it failed"
fi
There are few points which we need to keep in mind
use "" instead of ''
use -a instead of &&
put space before and after operator sign like [ a = b], don't use as [ a=b ] in if condition
Hence above solution worked for me !!!
Some times If you touch the keyboard accidentally and removed a space.
if [ "$myvar" = "something"]; then
do something
fi
Will trigger this error message. Note the space before ']' is required.
I have had same problem with my scripts. But when I did some modifications it worked for me. I did like this :-
export k=$(date "+%k");
if [ $k -ge 16 ]
then exit 0;
else
echo "good job for nothing";
fi;
that way I resolved my problem. Hope that will help for you too.

Resources