Getting Bad substitution error with a Shell Script on a mac? - macos

I'm getting an error message "./query.sh: line 5: ${1,,}: bad substitution" whenever I run a shell script in a Mac OSX terminal
#!/bin/bash
dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
if [ "$1" != "" ]; then
letter1=$(echo ${1,,}|(cut -b1))new
if [[ $letter1 == [a-zA-Z0-9] ]]; then
if [ -f "$dir/data/$letter1" ]; then
grep -ai "^$1" "$dir/data/$letter1"
else
letter2=$(echo ${1,,}|cut -b2)
if [[ $letter2 == [a-zA-Z0-9] ]]; then
if [ -f "$dir/data/$letter1/$letter2" ]; then
grep -ai "^$1" "$dir/data/$letter1/$letter2"
else
letter3=$(echo ${1,,}|cut -b3)
if [[ $letter3 == [a-zA-Z0-9] ]]; then
if [ -f "$dir/data/$letter1/$letter2/$letter3" ]; then
grep -ai "^$1" "$dir/data/$letter1/$letter2/$letter3"
fi
else
if [ -f "$dir/data/$letter1/$letter2/symbols" ]; then
grep -ai "^$1" "$dir/data/$letter1/$letter2/symbols"
fi
fi
fi
else
if [ -f "$dir/data/$letter1/symbols" ]; then
grep -ai "^$1" "$dir/data/$letter1/symbols"
fi
fi
fi
else
if [ -f "$dir/data/symbols" ]; then
grep -ai "^$1" "$dir/data/symbols"
fi
fi
else
echo "[*] Example: ./query name#domain.com"
fi
The scripts function is to search throuhg a huge number of data files so could anybody help me pinpoint the source of the problem ?

The ,, operator was introduced in bash 4.0, but /bin/bash on macOS is still version 3.2. You can install a newer version of bash and change your shebang accordingly, or you can use letter1=$(echo "$1" | tr '[:upper:]' '[:lower:]' | cut -b1) instead.
(You can, however, use ${letter:0:1}, ${letter:1:1}, etc, in place of a call to cut to get a single letter from the string.)
My advice is to treat /bin/bash on macOS as nothing more than a POSIX-compatible shell. Use #!/bin/sh if you want a portable script, or use #!/usr/local/bin/bash (or whatever is appropriate, after installing a new version of bash) if you want to take advantage of bash extensions at the expense of portability.

If you are using a subshell to produce the value anyway, you might as well change the call to something which is portable back to earlier versions of Bash.
Running cut -b1 in a subshell (in parentheses) is useless and doing it after the conversion means you convert a potentially long string and then discard everything except the first character.
letter1=$(echo "${1:0:1}" | tr '[:upper:]' '[:lower:]')new
Notice also the double quotes around the argument to echo.
Do you really want the suffix new to be attached to the value? The first condition in the if can never match in that case.
You should probably also refactor the code to avoid the deep "arrow antipattern" and only perform the grep once you have a value for the file name you want to search.

As mentioned in other answers, the builtin bash on Mac is very old and must be upgraded to use some operators, e.g. the caret operator. If you use homebrew, run brew install bash and follow the instructions in this post.

Related

how to check if a file exist and is a text file?

Hi everyone I need to check if a file exist with a shell script. I did some digging and ended up with this syntax but I'm not sure why it isn't working
(please bear in mind that you are talking to beginner)
I've found that you can add -e for example to check if it exist but I didn't get where these shortcuts came form or their names
#! /bin/bash
if [ "$#" = "1" ]
then
if [ -e $($1) ] && [ -f $($1) ]
then echo 'the file exists'
fi
fi
In idiomatic Bash:
#!/usr/bin/env bash
if [[ -f "${1-}" ]]
then
echo 'the file exists'
fi
Correct shebang
[[ rather than [
-f implies -e
No need for semicolons or single-use variables.
Please keep in mind that this does not tell you whether the file is a text file. The only "definition" of a text file as opposed to any other file is whether it contains only printable characters, and even that falls short of dealing with UTF BOM characters and non-ASCII character sets. For that you may want to look at the non-authoritative output of file "${1-}", for example:
$ file ~/.bashrc
/home/username/.bashrc: ASCII text
More in the Bash Guide.
#!/bin/bash
if [ "$#" == 1 ]; then
if [[ -e "$1" && -f "$1" ]]; then
echo 'The file exists';
fi
fi
You should put every conditional && between [[ ]] symbols otherwise it will be interpreted as execute if success.
#! /bin/sh
FILE=$1 # get filename from commandline
if [ -f $FILE ]; then
echo "file $FILE exists"
fi
See the fine manual page of test commands, which are built-in in the different shells: man test; man sh; man bash
You will also find many shell primers which explain this very nicely.
Or see bash reference manual: https://www.gnu.org/software/bash/manual/bash.pdf

How to check if the output of a command contains a string and then run a command if the string exists

Example
if "darwin" in $MACHTYPE;
then
echo "whoa it's a mac!"
fi
And the output should be
whoa it's a mac, if darwin is found in the output of $MACHTYPE
Please guide me!
Provided you're using bash, you could use the =~ operator:
if [[ "$MACHTYPE" =~ "darwin" ]];
then
echo "whoa it's a mac!"
fi
From the bash man page:
An additional binary operator, =~, is available, with the same precedence as == and !=. When it is used, the string to the right of the operator is considered an extended regular expression and matched accordingly (as in regex(3)).
If you don't have a version of bash which supports regular expressions then you can use globbing:
if [[ $MACHTYPE = *darwin* ]]
then
echo "whoa it's a mac!"
fi
Note that you must use [[, not [.
Other shells like sh might support [[ but that is not guaranteed by the standard.
You could evaluate your command directly, for example:
if uname -a | grep -i "darwin" > /dev/null; then
echo "it is a mac"
fi
In this case, grep will exit 0 if found a value and output will be redirected to /dev/null if try then you can call your command, in this case: echo "it is a mac"
The code below can get the output of your command on the cmd, then check if there has the specific word.
command="command here"
if[ `echo $command | grep -c "\"darwin\""` -gt 0 ]; then
Do anything you want here
fi

Using 'grep' in nested if-statement

I'm still finding my way around bash scripting so please bear with me.
At the moment I am trying to write a script that checks a few on a server.
Once check is to see if the GPU driver has is the latest version.
However regardless of the installed GPU driver on the server, the script returns GPU is not upgraded
Here is the code:
#!/bin/bash -x
######################################################
#GENERAL VARIABLES
GPU_DRIVER=270.41.19
######################################################
#Checking if Packsges are Installed
if [ $(uname -r) != $KERNEL_VERSION ]
then
echo "Kernel is not Upgraded"
#INSTALL KENRENL!
#REBOOT!
else if [ ! $(nvidia-smi -q |grep -q $GPU_DRIVER) ]
then
echo "GPU is not Upgraded"
else if [ $(cat /usr/ort/build_number) != $CODE_RELEASE ]
then
echo "Code Release526 Has not Been Installed"
fi
fi
fi
I would like to know why the condition in the if-statement does not apply?
NOTE:
The output of the nvidia-smi looks similar to below:
:~/script$ nvidia-smi -q|grep Driver
Driver Version : 270.41.19
Driver Model
You want to test whether a grep succeeded or failed. That does not require [...] or $(...). You merely need to execute the grep. Contrary to popular belief, [ is not part of the if statement syntax; it is a bash command which succeeds or fails based on the evaluation of a conditional expression. (Usually, you would want to use [[, which is a better conditional evaluator.) The if statement is followed by a series of ordinary bash statements; followed by the keyword then. If the last statement succeeds, the then branch is taken; otherwise the else branch is taken.
Change
else if [ ! $(nvidia-smi -q |grep -q $GPU_DRIVER) ]
to
elif ! nvidia-smi -q | grep -q -F "$GPU_DRIVER"; then
(And the elif will remove the need for the fi matching that if.)
Aside from removing the test built-in ([), I fixed a couple of other things:
grep normally expects patterns to be regexes. In a regex, a . matches any character. I think you are looking for a precise match, so I added the -F flag.
And I put quotes around the $GPU_DRIVER, just in case.
To explain the if ... then ... elif ... fi syntax, here's the entire if statement:
if [[ $(uname -r) != $KERNEL_VERSION ]]; then
echo "Kernel is not Upgraded"
#INSTALL KENRENL!
#REBOOT!
elif ! nvidia-smi -q |grep -q -F "$GPU_DRIVER"; then
echo "GPU is not Upgraded"
elif [[ $(cat /usr/ort/build_number) != $CODE_RELEASE ]]; then
echo "Code Release526 Has not Been Installed"
fi
The grep -q does't return/print anything. It actually sets the return value as 0 or 1. You can check this using $?. So effectively your if statement becomes
[ ! $() ]
The $() returns false always. This results in the behavior you have defined.

Test if a command outputs an empty string

How can I test if a command outputs an empty string?
Previously, the question asked how to check whether there are files in a directory. The following code achieves that, but see rsp's answer for a better solution.
Empty output
Commands don’t return values – they output them. You can capture this output by using command substitution; e.g. $(ls -A). You can test for a non-empty string in Bash like this:
if [[ $(ls -A) ]]; then
echo "there are files"
else
echo "no files found"
fi
Note that I've used -A rather than -a, since it omits the symbolic current (.) and parent (..) directory entries.
Note: As pointed out in the comments, command substitution doesn't capture trailing newlines. Therefore, if the command outputs only newlines, the substitution will capture nothing and the test will return false. While very unlikely, this is possible in the above example, since a single newline is a valid filename! More information in this answer.
Exit code
If you want to check that the command completed successfully, you can inspect $?, which contains the exit code of the last command (zero for success, non-zero for failure). For example:
files=$(ls -A)
if [[ $? != 0 ]]; then
echo "Command failed."
elif [[ $files ]]; then
echo "Files found."
else
echo "No files found."
fi
More info here.
TL;DR
if [[ $(ls -A | head -c1 | wc -c) -ne 0 ]]; then ...; fi
Thanks to netj
for a suggestion to improve my original:if [[ $(ls -A | wc -c) -ne 0 ]]; then ...; fi
This is an old question but I see at least two things that need some improvement or at least some clarification.
First problem
First problem I see is that most of the examples provided here simply don't work. They use the ls -al and ls -Al commands - both of which output non-empty strings in empty directories. Those examples always report that there are files even when there are none.
For that reason you should use just ls -A - Why would anyone want to use the -l switch which means "use a long listing format" when all you want is test if there is any output or not, anyway?
So most of the answers here are simply incorrect.
Second problem
The second problem is that while some answers work fine (those that don't use ls -al or ls -Al but ls -A instead) they all do something like this:
run a command
buffer its entire output in RAM
convert the output into a huge single-line string
compare that string to an empty string
What I would suggest doing instead would be:
run a command
count the characters in its output without storing them
or even better - count the number of maximally 1 character using head -c1(thanks to netj for posting this idea in the comments below)
compare that number with zero
So for example, instead of:
if [[ $(ls -A) ]]
I would use:
if [[ $(ls -A | wc -c) -ne 0 ]]
# or:
if [[ $(ls -A | head -c1 | wc -c) -ne 0 ]]
Instead of:
if [ -z "$(ls -lA)" ]
I would use:
if [ $(ls -lA | wc -c) -eq 0 ]
# or:
if [ $(ls -lA | head -c1 | wc -c) -eq 0 ]
and so on.
For small outputs it may not be a problem but for larger outputs the difference may be significant:
$ time [ -z "$(seq 1 10000000)" ]
real 0m2.703s
user 0m2.485s
sys 0m0.347s
Compare it with:
$ time [ $(seq 1 10000000 | wc -c) -eq 0 ]
real 0m0.128s
user 0m0.081s
sys 0m0.105s
And even better:
$ time [ $(seq 1 10000000 | head -c1 | wc -c) -eq 0 ]
real 0m0.004s
user 0m0.000s
sys 0m0.007s
Full example
Updated example from the answer by Will Vousden:
if [[ $(ls -A | wc -c) -ne 0 ]]; then
echo "there are files"
else
echo "no files found"
fi
Updated again after suggestions by netj:
if [[ $(ls -A | head -c1 | wc -c) -ne 0 ]]; then
echo "there are files"
else
echo "no files found"
fi
Additional update by jakeonfire:
grep will exit with a failure if there is no match. We can take advantage of this to simplify the syntax slightly:
if ls -A | head -c1 | grep -E '.'; then
echo "there are files"
fi
if ! ls -A | head -c1 | grep -E '.'; then
echo "no files found"
fi
Discarding whitespace
If the command that you're testing could output some whitespace that you want to treat as an empty string, then instead of:
| wc -c
you could use:
| tr -d ' \n\r\t ' | wc -c
or with head -c1:
| tr -d ' \n\r\t ' | head -c1 | wc -c
or something like that.
Summary
First, use a command that works.
Second, avoid unnecessary storing in RAM and processing of potentially huge data.
The answer didn't specify that the output is always small so a possibility of large output needs to be considered as well.
if [ -z "$(ls -lA)" ]; then
echo "no files found"
else
echo "There are files"
fi
This will run the command and check whether the returned output (string) has a zero length.
You might want to check the 'test' manual pages for other flags.
Use the "" around the argument that is being checked, otherwise empty results will result in a syntax error as there is no second argument (to check) given!
Note: that ls -la always returns . and .. so using that will not work, see ls manual pages. Furthermore, while this might seem convenient and easy, I suppose it will break easily. Writing a small script/application that returns 0 or 1 depending on the result is much more reliable!
For those who want an elegant, bash version-independent solution (in fact should work in other modern shells) and those who love to use one-liners for quick tasks. Here we go!
ls | grep . && echo 'files found' || echo 'files not found'
(note as one of the comments mentioned, ls -al and in fact, just -l and -a will all return something, so in my answer I use simple ls
Bash Reference Manual
6.4 Bash Conditional Expressions
-z string
True if the length of string is zero.
-n string
string
True if the length of string is non-zero.
You can use shorthand version:
if [[ $(ls -A) ]]; then
echo "there are files"
else
echo "no files found"
fi
As Jon Lin commented, ls -al will always output (for . and ..). You want ls -Al to avoid these two directories.
You could for example put the output of the command into a shell variable:
v=$(ls -Al)
An older, non-nestable, notation is
v=`ls -Al`
but I prefer the nestable notation $( ... )
The you can test if that variable is non empty
if [ -n "$v" ]; then
echo there are files
else
echo no files
fi
And you could combine both as if [ -n "$(ls -Al)" ]; then
Sometimes, ls may be some shell alias. You might prefer to use $(/bin/ls -Al). See ls(1) and hier(7) and environ(7) and your ~/.bashrc (if your shell is GNU bash; my interactive shell is zsh, defined in /etc/passwd - see passwd(5) and chsh(1)).
I'm guessing you want the output of the ls -al command, so in bash, you'd have something like:
LS=`ls -la`
if [ -n "$LS" ]; then
echo "there are files"
else
echo "no files found"
fi
sometimes "something" may come not to stdout but to the stderr of the testing application, so here is the fix working more universal way:
if [[ $(partprobe ${1} 2>&1 | wc -c) -ne 0 ]]; then
echo "require fixing GPT parititioning"
else
echo "no GPT fix necessary"
fi
Here's a solution for more extreme cases:
if [ `command | head -c1 | wc -c` -gt 0 ]; then ...; fi
This will work
for all Bourne shells;
if the command output is all zeroes;
efficiently regardless of output size;
however,
the command or its subprocesses will be killed once anything is output.
All the answers given so far deal with commands that terminate and output a non-empty string.
Most are broken in the following senses:
They don't deal properly with commands outputting only newlines;
starting from Bash≥4.4 most will spam standard error if the command output null bytes (as they use command substitution);
most will slurp the full output stream, so will wait until the command terminates before answering. Some commands never terminate (try, e.g., yes).
So to fix all these issues, and to answer the following question efficiently,
How can I test if a command outputs an empty string?
you can use:
if read -n1 -d '' < <(command_here); then
echo "Command outputs something"
else
echo "Command doesn't output anything"
fi
You may also add some timeout so as to test whether a command outputs a non-empty string within a given time, using read's -t option. E.g., for a 2.5 seconds timeout:
if read -t2.5 -n1 -d '' < <(command_here); then
echo "Command outputs something"
else
echo "Command doesn't output anything"
fi
Remark. If you think you need to determine whether a command outputs a non-empty string, you very likely have an XY problem.
Here's an alternative approach that writes the std-out and std-err of some command a temporary file, and then checks to see if that file is empty. A benefit of this approach is that it captures both outputs, and does not use sub-shells or pipes. These latter aspects are important because they can interfere with trapping bash exit handling (e.g. here)
tmpfile=$(mktemp)
some-command &> "$tmpfile"
if [[ $? != 0 ]]; then
echo "Command failed"
elif [[ -s "$tmpfile" ]]; then
echo "Command generated output"
else
echo "Command has no output"
fi
rm -f "$tmpfile"
Sometimes you want to save the output, if it's non-empty, to pass it to another command. If so, you could use something like
list=`grep -l "MY_DESIRED_STRING" *.log `
if [ $? -eq 0 ]
then
/bin/rm $list
fi
This way, the rm command won't hang if the list is empty.
As mentioned by tripleee in the question comments , use moreutils ifne (if input not empty).
In this case we want ifne -n which negates the test:
ls -A /tmp/empty | ifne -n command-to-run-if-empty-input
The advantage of this over many of the another answers when the output of the initial command is non-empty. ifne will start writing it to STDOUT straight away, rather than buffering the entire output then writing it later, which is important if the initial output is slowly generated or extremely long and would overflow the maximum length of a shell variable.
There are a few utils in moreutils that arguably should be in coreutils -- they're worth checking out if you spend a lot of time living in a shell.
In particular interest to the OP may be dirempty/exists tool which at the time of writing is still under consideration, and has been for some time (it could probably use a bump).

Shell script to iterate files. Got error on line 3

#!/bin/bash
for dir in /home/username/git/*/
do
for file in "$dir"/*
do
if [[ -f $file ]]
then
echo "$file"
fi
done
done
When I try to run it. I got
syntax error near unexpected toke' `do
'rocTest.sh: line 3: `do
Why?
Use "$file" (with quotes) consistently to deal with "problematic" file names; in particular if [[ -f $file ]] should be
if [[ -f "$file" ]] ...
Note that bash is not always in /bin (e.g. FreeBSD places it in /usr/local/bin); for wider portability, either use
#!/usr/bin/env bash
or #!/bin/sh and make sure to remove bash-isms (e.g. use checkbashisms on Debian/Ubuntu). E.g. write if test -f "$file" instead of [[ -f "$file" ]]

Resources