bash run read a file and run the read line OR command - bash

two commands with OR condition
test -e a.txt || test -e b.txt this command running without any problem from CLI but if I read from a file and try to run it gives sh: ||: unknown operand' error
cat test.txt
test -e a.txt || test -e b.txt
Read and Run the command
cat test.txt| while read command; do $command;done
sh: ||: unknown operand
Any thoughts

Very simplified, bash will:
Parse a command or structure, then for each command:
Apply brace expansion
Apply parameter expansion
Do word splitting
Apply pathname expansion
Execute the result
Handling of || happens during parsing in step 1, but you expand it in step 3. As a result, it's treated as a regular string as if running test -e a.txt "||" test -e b.txt.
It will similarly fail for commands like echo {1..10} which would require re-doing #2, and echo $PATH which would require re-doing #3.
Meanwhile, it will work for echo Hello (#4) and ls *.png (#4/#5) because these only use features that come after.
While having a command in a string is a code smell indicating that you're painting yourself into an awkward corner, you can use eval to apply all the steps over from #1 on a string of your choice:
cmd="test -e a.txt || test -e b.txt"
eval "$cmd"

Related

Replance extension for shell script error

I am trying to write shell script (sh), Where I am getting below error
variable i contains:
test.txt
code:
echo "${i/.txt/}"
Error:
just.sh: 16: just.sh: Bad substitution
expected output string :
text
Reproduce steps
Create file:
touch text.txt
Create file test.sh contents using any of editor
code:
#!/bin/sh
for i in `find *.txt`
do
echo "$i"
echo "${i/.txt/}"
done
How to run:
sh test.sh
sh is not bash. Fix your shebang (the 1st line) as #!/bin/bash first.
References
Difference between sh and bash, search "expansion" in the thread
Bash features a rich set of expanded non-standard parameter expansions such as ${substring:1:2}, ${variable/pattern/replacement}, case conversion, etc.

grep output different in bash script

I am creating a bash script that will simply use grep to look through a bunch of logs for a certain string.
Something interesting happens though.
For the purpose of testing all of the log files the files are named test1.log, test2.log, test3.log, etc.
When using the grep command:
grep -oHnR TEST Logs/test*
The output contains all instances from all files in the folder as expected.
But when using a command but contained in the bash script below:
#!/bin/bash
#start
grep -oHnR $1 $2
#end
The output displays the instances from only 1 file.
When running the script I am using the following command:
bash test.bash TEST Logs/test*
Here is an example of the expected output (what occurs when simply using grep):
Logs/test2.log:8:TEST
Logs/test2.log:20:TEST
Logs/test2.log:41:TEST
Logs/test.log:2:TEST
Logs/test.log:18:TEST
and here is an example of the output received when using the bash script:
Logs/test2.log:8:TEST
Logs/test2.log:20:TEST
Logs/test2.log:41:TEST
Can someone explain to me why this happens?
When you call the line
bash test.bash TEST Logs/test*
this will be translated by the shell to
bash test.bash TEST Logs/test1.log Logs/test2.log Logs/test3.log Logs/test4.log
(if you have four log files).
The command line parameters TEST, Logs/test1.log, Logs/test2.log, etc. will be given the names $1, $2, $3, etc.; $1 will be TEST, $2 will be Logs/test1.log.
You just ignore the remaining parameters and use just one log file when you use $2 only.
A correct version would be this:
#!/bin/bash
#start
grep -oHnR "$#"
#end
This will pass all the parameters properly and also take care of nastinesses like spaces in file names (your version would have had trouble with these).
To understand what's happening, you can use a simpler script:
#!/bin/bash
echo $1
echo $2
That outputs the first two arguments, as you asked for.
You want to use the first argument, and then use all the rest as input files. So use shift like this:
#!/bin/bash
search=$1
shift
echo "$1"
echo "$#"
Notice also the use of double quotes.
In your case, because you want the search string and the filenames to be passed to grep in the same order, you don't even need to shift:
#!/bin/bash
grep -oHnR -e "$#"
(I added the -e in case the search string begins with -)
The unquoted * is being affected by globbing when you are calling the script.
Using set -x to output what is running from the script makes this more clear.
$ ./greptest.sh TEST test*
++ grep -oHnR TEST test1.log
$ ./greptest.sh TEST "test*"
++ grep -oHnR TEST test1.log test2.log test3.log
In the first case, bash is expanding the * into the list of file names versus the second case it is being passed to grep. In the first case you actually have >2 args (as each filename expanded would become an arg) - adding echo $# to the script shows this too:
$ ./greptest.sh TEST test*
++ grep -oHnR TEST test1.log
++ echo 4
4
$ ./greptest.sh TEST "test*"
++ grep -oHnR TEST test1.log test2.log test3.log
++ echo 2
2
You probably want to escape the wildcard on your bash invocation:
bash test.bash TEST Logs/test\*
That way it'll get passed through to grep as an *, otherwise the shell will have expanded it to every file in the Logs dir whose name starts with test.
Alternatively, change your script to allow more than one file on the command line:
#!/bin/bash
hold=$1
shift
grep -oHnR $hold $#

How do I execute a sed command from a Bash variable?

I have a sed command that works when executed directly.
echo foo > /home/user/bar
sed -i 's/foo/zoo/' /home/user/bar
It also works when directly embedded in $(...) or `...`.
However, if I try to execute it from a Bash variable, I get an error:
CMD="sed -i 's/foo/zoo/' /home/user/bar"
$CMD
Error:
sed: -e expression #1, char 1: unknown command: `''
It also works if I echo it out and source the file:
echo $CMD > file
source file
What's going on here, and how do I get the sed command to run from a Bash variable?
Don't store full command a string variable to avoid word splitting. Use an array or shell function:
# store command in an array
cmd=(sed -i 's/foo/zoo/' /home/user/bar)
# execute the command
"${cmd[#]}"
Or else use a shell function:
fn() {
sed -i 's/foo/zoo/' /home/user/bar
}
#call it as:
fn
Read this BASH FAQ: I'm trying to put a command in a variable, but the complex cases always fail!
use eval : I am not sure why you want to do it. Note that using eval is a fragil solution and can cause your script to broke.
eval $CMD

All arguments into files with correct quoting using "$#"

I need my bashscript to cat all of its parameters into a file. I tried to use cat for this because I need to add a lot of lines:
#!/bin/sh
cat > /tmp/output << EOF
I was called with the following parameters:
"$#"
or
$#
EOF
cat /tmp/output
Which leads to the following output
$./test.sh "dsggdssgd" "dsggdssgd dgdsdsg"
I was called with the following parameters:
"dsggdssgd dsggdssgd dgdsdsg"
or
dsggdssgd dsggdssgd dgdsdsg
I want neither of these two things: I need the exact quoting which was used on the command line. How can I achieve this? I always thought $# does everything right in regards to quoting.
Well, you are right that "$#" has the args including the whitespace in each arg. However, since the shell performs quote removal before executing a command, you can never know how exactly the args were quoted (e.g. whether with single or double quotes, or backslashes or any combination thereof--but you shouldn't need to know, since all you should care for are the argument values).
Placing "$#" in a here-document is pointless because you lose the information about where each arg starts and ends (they're joined with a space inbetween). Here's a way to see just this:
$ cat test.sh
#!/bin/sh
printf 'I was called with the following parameters:\n'
printf '"%s"\n' "$#"
$ ./test.sh "dsggdssgd" "dsggdssgd dgdsdsg"
I was called with the following parameters:
"dsggdssgd"
"dsggdssgd dgdsdsg"
Try:
#!/bin/bash
for x in "$#"; do echo -ne "\"$x\" "; done; echo
To see what's interpreted by Bash, use:
bash -x ./script.sh
or add this to the beginning of your script:
set -x
You might want add this on the parent script.

Why do I get a “Can't find string terminator” error only when the command is in a variable?

I wrote a simple shell script to get the version of Perl modules installed
on a server and I keep receiving the following error:
Can't find string terminator "'" anywhere before EOF at -e line 1.
Here is my script:
#!/bin/sh
#
mod_name="Sub::Uplevel"
tmp1="perl -M$mod_name -e 'print \"\$$mod_name::VERSION\"'"
echo $tmp1
$tmp1
If I just directly run the echo'd line (perl -MSub::Uplevel -e 'print "$Sub::Uplevel::VERSION"'), it works. Why doesn't the line work when its run from the variable $tmp1?
In place of just $tmp1, eval works:
eval "$tmp1"
That's because splitting a variable into words (for arguments) is done strictly by splitting on $IFS, not the normal input-parsing. eval forces the normal input parsing.
How did I figure this out?
Change your tmp1= line to put an echo in front, and you get:
perl -MSub::Uplevel -e 'print "$Sub::Uplevel::VERSION"'
Note that the ' are still there, which you wouldn't expect. If you write a quick script:
#!/bin/sh
for a in "$#"; do
echo "arg: $a"
done
and put a call to that in place of echo, you find how the arguments are really split:
arg: perl
arg: -MSub::Uplevel
arg: -e
arg: 'print
arg: "$Sub::Uplevel::VERSION"'
So, you can see that's splitting on spaces, so IFS.
It's always better to construct commands using bash arrays. That will keep arguments with whitespace properly grouped:
#!/bin/bash
mod_name="Sub::Uplevel"
perl_script=$(printf 'print "$%s::VERSION"' $mod_name)
tmp1=(perl -M$mod_name -e "$perl_script")
echo "${tmp1[#]}"
output=$( "${tmp1[#]}" )
Arrays are a bash feature, so the shebang line must reference bash not sh.
I'd usually write what you are doing with backticks, to run the command inside the shell:
#!/bin/sh
#
mod_name="Sub::Uplevel"
tmp1=`perl -M$mod_name -e 'print \"\$$mod_name::VERSION\"'`
echo $tmp1
Then you can work on $tmp1 as needed. It also avoids dealing with escaping.
Try to execute the script the below way(debugging the script):
sh -vx your_script.sh
Then you would be able to see where exactly the problem is.
I donot have the shell to execute it right now.

Resources