Bash convention for if ; then - bash

From this web page :
http://tldp.org/LDP/abs/html/abs-guide.html
It's mentioned the usage of the if bracket then convention which need a space after the semicolon :
;
Command separator [semicolon]. Permits putting two or more commands on the same line.
echo hello; echo there
if [ -x "$filename" ]; then # Note the space after the semicolon.
#+ ^^
echo "File $filename exists."; cp $filename $filename.bak
else # ^^
echo "File $filename not found."; touch $filename
fi; echo "File test complete."
Note that the ";" sometimes needs to be escaped.
Does anyone know where is this coming from and if this is needed at all by certain shells?

This has become the style in the last few years:
if [ -x "$filename" ]; then
echo "hi"
fi
However, back when dinosaurs like Burroughs and Sperry Rand ruled the earth, I learned to write if statements like this:
if [ -x "$filename" ]
then
echo "hi"
fi
Then, you don't even need a semicolon.
The new style with then on the same line as the if started in order to emulate the way C and other programming languages did their if statements:
if (! strcmp("foo", "bar")) {
printf "Strings equal\n";
}
These programming languages put the opening curly brace on the same line as the if.

Semicolon ; is an operator (not a keyword, like braces { }or a bang !) in Shell, so it doesn't need to be delimited with white space to be recognized in any POSIX-compliant shell.
However, doing so improves readability (for my taste).
Semicolon needs to be escaped if you mean a symbol "semicolon", not an operator.

The space after the semicolon is not required by the syntax for any shell I know of, but it's good style and makes the code easier to read.
I suppose the "sometimes needs to be escaped" wording refers to cases like echo foo\;bar, where you don't want the semicolon to be interpreted as a separator by the shell.

I do not believe that the space should be necessary there. There's nothing about requiring spaces in the POSIX sh spec.
Empirically, the following works fine in both bash 4.1.5(1) and dash:
$ if true;then echo hi;else echo bye;fi
hi
$

I've never came across a shell that required a space in that context.
Just to make sure, I've asked on c.u.s., you can read the replies here.

Related

why in an 'if' statement 'then' has to be in the next line in bash?

if is followed by then in bash but I don't understand why then cannot be used in the same line like if [...] then it has to be used in the next line. Does that remove some ambiguity from the code? or bash is designed like that? what is the underlying reason for it?
I tried to write if and then in the same line but it gave the error below:
./test: line 6: syntax error near unexpected token \`fi'
./test: line 6: \`fi'
the code is:
#!/bin/bash
if [ $1 -gt 0 ] then
echo "$1 is positive"
fi
It has to be preceded by a separator of some description, not necessarily on the next line(a). In other words, to achieve what you want, you can simply use:
if [[ $1 -gt 0 ]] ; then
echo "$1 is positive"
fi
As an aside, for one-liners like that, I tend to prefer:
[[ $1 -gt 0 ]] && echo "$1 is positive"
But that's simply because I prefer to see as much code on screen as possible. It's really just a style thing which you can freely ignore.
(a) The reason for this can be found in the Bash manpage (my emphasis):
RESERVED WORDS: Reserved words are words that have a special meaning to the shell. The following words are recognized as reserved when unquoted and either the first word of a simple command (see SHELL GRAMMAR below) or the third word of a case or for command:
! case coproc do done elif else esac fi for function if in select then until while { } time [[ ]]
Note that, though that section states it's the "first word of a simple command", the manpage seems to contradict itself in the referenced SHELL GRAMMAR section:
A simple command is a sequence of optional variable assignments followed by blank-separated words and redirections, and terminated by a control operator. The first word specifies the command to be executed, and is passed as argument zero.
So, whether you consider it part of the next command or a separator of some sort is arguable. What is not arguable is that it needs a separator of some sort (newline or semicolon, for example) before the then keyword.
The manpage doesn't go into why it was designed that way but it's probably to make the parsing of commands a little simpler.
Here's another way to explain the need for a line break or semicolon before then: the thing that goes between if and then is a command (or sequence of commands); if the then just came directly after the command without a delimiter, it'd be ambiguous whether it should be treated as a shell keyword or just an argument to the command.
For instance, this is a perfectly valid command:
echo This prints a phrase ending with then
...which prints "This prints a phrase ending with then". Now, consider this one:
if echo This prints a phrase ending with then
should that print "This prints a phrase ending with then" and look for a then keyword later on, or should it just print "This prints a phrase ending with" and treat the then as a keyword?
In order to settle this ambiguity, shell syntax says it should treat "then" as an argument to echo, and in order to get it treated as a keyword you need a command delimiter (line break or semicolon) to mark the end of the command.
Now, you might think that your if condition [ $1 -gt 0 ], already has a perfectly good delimiter, namely the ]. But in shell syntax, that's really just an argument to the [ command (yes, that's a command). Try this command:
[ 1 -gt 0 ] then
...and you'll probably get an error like "-bash: [: missing ']'", because the [ command checked its last argument to make sure it was "]", found that it was "then" instead, and panicked.
Perhaps it helps to understand why this is so by way of a few examples. The argument to if is a sequence of commands; so you can say e.g.
if read -r -p "What is your name?" name
[ "$name" -eq "tripleee" ]
then
echo "I kneel before thee"
fi
or even a complex compound like
while read -r -p "Favorite number?" number
case $number in
42) true; break;;
*) false;;
esac
do
echo "Review your preferences, then try again"
done
This extremely powerful but potentially confusing feature of the shell is probably one of its most misunderstood constructs. The ability to pass a sequence of commands to the flow control statements can make for very elegant scripts, but is often missed entirely (see e.g. Why is testing "$?" to see if a command succeeded or not, an anti-pattern?)
If it helps, you can use semi-colons
if [ $1 -gt 0 ]; then
echo "$1 is positive"
fi
# or even
if [ $1 -gt 0 ]; then echo "$1 is positive"; fi
As for why, it helps me to think of if, then, else, and fi as bash commands, and just like all other commands, they need to be at the start of a line (or after a semi-colon).

How to write the content of a unknown shell variable to stdout in a safe way

I only know what I have read so far, and I am confused about how to actually echo a variable as is.
echo "$var" might fail if var='-n'
printf '%s\n' "$var" might fail because of shell not implementig printf
echo -- "$var" might fail because it is a gnu extension
So if i would have to guess:
echo x"$var"|sed 's#^x##1' would be the only way, but I have never encountered that pattern. Why?
As a concrete question:
for source; do
target="$(echo "$source"|sed 's#[^a-z0-9]\+#.#')"
# do stuff with $source and $target
done
Does this work, or could someone "hack" / "break" my script by putting a file named '-n' somewhere, assuming my script is executed by some my_script * cron?
How do I write echo "$var" so it does not break?
Does this work, or could someone "hack" / "break" my script by putting
a file named '-n' somewhere?
There is nothing wrong with:
target="$(echo "$source"|sed 's#[^a-z0-9]\+#.#')"
What is happening:
"$(...)" is a command substitution which will substitute the results of the command within as the value -- in which case the result is assigned to target.
echo "$source"|sed 's#[^a-z0-9]\+#.#' simply pipes the output of echo (e.g. what is in source) to sed for the simple substitution of every character not lowercase or a digit followed by + with a period 1. Note: the quotes ".." around $source ARE proper within the command substitution.
There is no inherent reason assigning -n to a variable will cause any mischief. What you do with the variable is another question, but suffice it to say it is hard to see any problem.
"POSIX-shell's out there not implementing printf" -- Huh? Any shell not implementing printf would be more an exception rather than the rule. See printf - The Open Group Library that is POSIX.
If you are attempting to printf output that begins with '-' simply precede the output with "--" to indicate End-of-Options before the string your want to print and things will go fine. With your example of "-n", printf is about the only way you will output a variable beginning with the single '-', for example:
$ t="-n"
$ printf -- "%s\n" "$t"
-n
(note: you don't have to include "--" in printf "%s\n" "$var", the only time you must include it is with printf -- "-foo\n" or you will receive an "invalid option error".
For echo you can enable interpretation of backslash escapes with -e and include a backspace, e.g.
$ echo -e " \b$t"
-n
I think that has covered all issues. If not, let me know. Also, if you have any additional questions, drop a comment below or edit and add to your question.
footnotes:
note: + isn't part of basic regular expressions and it need not be escaped, but if there is any question, it is safer to include in a character class of its own, e.g. [^a-z0-9][+].

shell script how to compare file name with expected filename but different extention in single line

I have doubt in shell script
I will describe the scenario, $file is containing the file name of my interest,
consider $file can contain foo.1, foo.2, foo.3 here foo will be constant,
but .1,.2,.3 will change, i want to test this in single line in if statement something like
if [ $file = "foo.[1-9]" ]; then
echo "File name is $file"
fi'
i know above script doesn't work :) can anyone suggest what should i refer for this ?
Trim any extension, then see if it's "foo"?
base=${file%.[1-9]}
if [ "$base" = "foo" ]; then
echo Smashing success
fi
Equivalently, I always like to recommend case because it's portable and versatile.
case $file in
foo.[1-9] ) echo Smashing success ;;
esac
The syntax may seem weird at first but it's well worth knowing.
Both of these techniques should be portable to any Bourne-compatible shell, including Dash and POSIX sh.
Use [[ instead for regex matching.
if [[ $file =~ ^foo\.[1-9]$ ]] ; ...

POSIX sh equivalent for Bash’s printf %q

Suppose I have a #!/bin/sh script which can take a variety of positional parameters, some of which may include spaces, either/both kinds of quotes, etc. I want to iterate "$#" and for each argument either process it immediately somehow, or save it for later. At the end of the script I want to launch (perhaps exec) another process, passing in some of these parameters with all special characters intact.
If I were doing no processing on the parameters, othercmd "$#" would work fine, but I need to pull out some parameters and process them a bit.
If I could assume Bash, then I could use printf %q to compute quoted versions of args that I could eval later, but this would not work on e.g. Ubuntu's Dash (/bin/sh).
Is there any equivalent to printf %q that can be written in a plain Bourne shell script, using only built-ins and POSIX-defined utilities, say as a function I could copy into a script?
For example, a script trying to ls its arguments in reverse order:
#!/bin/sh
args=
for arg in "$#"
do
args="'$arg' $args"
done
eval "ls $args"
works for many cases:
$ ./handle goodbye "cruel world"
ls: cannot access cruel world: No such file or directory
ls: cannot access goodbye: No such file or directory
but not when ' is used:
$ ./handle goodbye "cruel'st world"
./handle: 1: eval: Syntax error: Unterminated quoted string
and the following works fine but relies on Bash:
#!/bin/bash
args=
for arg in "$#"
do
printf -v argq '%q' "$arg"
args="$argq $args"
done
eval "ls $args"
This is absolutely doable.
The answer you see by Jesse Glick is approximately there, but it has a couple of bugs, and I have a few more alternatives for your consideration, since this is a problem I ran into more than once.
First, and you might already know this, echo is a bad idea, one should use printf instead, if the goal is portability: "echo" has undefined behavior in POSIX if the argument it receives is "-n", and in practice some implementations of echo treat -n as a special option, while others just treat it as a normal argument to print. So that becomes this:
esceval()
{
printf %s "$1" | sed "s/'/'\"'\"'/g"
}
Alternatively, instead of escaping embedded single quotes by making them into:
'"'"'
..instead you could turn them into:
'\''
..stylistic differences I guess (I imagine performance difference is negligible either way, though I've never tested). The resulting sed string looks like this:
esceval()
{
printf %s "$1" | sed "s/'/'\\\\''/g"
}
(It's four backslashes because double quotes swallow two of them, and leaving two, and then sed swallows one, leaving just the one. Personally, I find this way more readable so that's what I'll use in the rest of the examples that involve it, but both should be equivalent.)
BUT, we still have a bug: command substitution will delete at least one (but in many shells ALL) of the trailing newlines from the command output (not all whitespace, just newlines specifically). So the above solution works unless you have newline(s) at the very end of an argument. Then you'll lose that/those newline(s). The fix is obviously simple: Add another character after the actual command value before outputting from your quote/esceval function. Incidentally, we already needed to do that anyway, because we needed to start and stop the escaped argument with single quotes. You have two alternatives:
esceval()
{
printf '%s\n' "$1" | sed "s/'/'\\\\''/g; 1 s/^/'/; $ s/$/'/"
}
This will ensure the argument comes out already fully escaped, no need for adding more single quotes when building the final string. This is probably the closest thing you will get to a single, inline-able version. If you're okay with having a sed dependency, you can stop here.
If you're not okay with the sed dependency, but you're fine with assuming that your shell is actually POSIX-compliant (there are still some out there, notably the /bin/sh on Solaris 10 and below, which won't be able to do this next variant - but almost all shells you need to care about will do this just fine):
esceval()
{
printf \'
unescaped=$1
while :
do
case $unescaped in
*\'*)
printf %s "${unescaped%%\'*}""'\''"
unescaped=${unescaped#*\'}
;;
*)
printf %s "$unescaped"
break
esac
done
printf \'
}
You might notice seemingly redundant quoting here:
printf %s "${unescaped%%\'*}""'\''"
..this could be replaced with:
printf %s "${unescaped%%\'*}'\''"
The only reason I do the former, is because one upon a time there were Bourne shells which had bugs when substituting variables into quoted strings where the quote around the variable didn't exactly start and end where the variable substitution did. Hence it's a paranoid portability habit of mine. In practice, you can do the latter, and it won't be a problem.
If you don't want to clobber the variable unescaped in the rest of your shell environment, then you can wrap the entire contents of that function in a subshell, like so:
esceval()
{
(
printf \'
unescaped=$1
while :
do
case $unescaped in
*\'*)
printf %s "${unescaped%%\'*}""'\''"
unescaped=${unescaped#*\'}
;;
*)
printf %s "$unescaped"
break
esac
done
printf \'
)
}
"But wait", you say: "What I want to do this on MULTIPLE arguments in one command? And I want the output to still look kinda nice and legible for me as a user if I run it from the command line for whatever reason."
Never fear, I have you covered:
esceval()
{
case $# in 0) return 0; esac
while :
do
printf "'"
printf %s "$1" | sed "s/'/'\\\\''/g"
shift
case $# in 0) break; esac
printf "' "
done
printf "'\n"
}
..or the same thing, but with the shell-only version:
esceval()
{
case $# in 0) return 0; esac
(
while :
do
printf "'"
unescaped=$1
while :
do
case $unescaped in
*\'*)
printf %s "${unescaped%%\'*}""'\''"
unescaped=${unescaped#*\'}
;;
*)
printf %s "$unescaped"
break
esac
done
shift
case $# in 0) break; esac
printf "' "
done
printf "'\n"
)
}
In those last four, you could collapse some of the outer printf statements and roll their single quotes up into another printf - I kept them separate because I feel it makes the logic more clear when you can see the starting and ending single-quotes on separate print statements.
P.S. There's also this monstrosity I made, which is a polyfill which will select between the previous two versions depending on if your shell seems to be capable of supporting the necessary variable substitution syntax (it looks awful though, because the shell-only version has to be inside an eval-ed string to keep the incompatible shells from barfing when they see it): https://github.com/mentalisttraceur/esceval/blob/master/sh/esceval.sh
I think this is POSIX. It works by clearing $# after expanding it for the for loop, but only once so that we can iteratively build it back up (in reverse) using set.
flag=0
for i in "$#"; do
[ "$flag" -eq 0 ] && shift $#
set -- "$i" "$#"
flag=1
done
echo "$#" # To see that "$#" has indeed been reversed
ls "$#"
I realize reversing the arguments was just an example, but you may be able to use this trick of set -- "$arg" "$#" or set -- "$#" "$arg" in other situations.
And yes, I realize I may have just reimplemented (poorly) ormaaj's Push.
Push. See the readme for examples.
The following seems to work with everything I have thrown at it so far, including spaces, both kinds of quotes and a variety of other metacharacters, and embedded newlines:
#!/bin/sh
quote() {
echo "$1" | sed "s/'/'\"'\"'/g"
}
args=
for arg in "$#"
do
argq="'"`quote "$arg"`"'"
args="$argq $args"
done
eval "ls $args"
If you're okay with calling out to an external executable (as in the sed solutions given in other answers), then you may as well call out to /usr/bin/printf. While it's true that the POSIX shell built-in printf doesn't support %q, the printf binary from Coreutils sure does (since release 8.25).
esceval() {
/usr/bin/printf '%q ' "$#"
}
We can use /usr/bin/printf when version of GNU Coreutil is not less than 8.25
#!/bin/sh
minversion="8.25"
gnuversion=$(ls '--version' | sed '1q' | awk 'NF{print $NF}')
printcmd="printf"
if ! [ $gnuversion \< $minversion ]; then
printcmd="/usr/bin/printf"
fi;
params=$($printcmd "%q" "$#")

Echo newline in Bash prints literal \n

How do I print a newline? This merely prints \n:
$ echo -e "Hello,\nWorld!"
Hello,\nWorld!
Use printf instead:
printf "hello\nworld\n"
printf behaves more consistently across different environments than echo.
Make sure you are in Bash.
$ echo $0
bash
All these four ways work for me:
echo -e "Hello\nworld"
echo -e 'Hello\nworld'
echo Hello$'\n'world
echo Hello ; echo world
echo $'hello\nworld'
prints
hello
world
$'' strings use ANSI C Quoting:
Words of the form $'string' are treated specially. The word expands to string, with backslash-escaped characters replaced as specified by the ANSI C standard.
You could always do echo "".
For example,
echo "Hello,"
echo ""
echo "World!"
On the off chance that someone finds themselves beating their head against the wall trying to figure out why a coworker's script won't print newlines, look out for this:
#!/bin/bash
function GET_RECORDS()
{
echo -e "starting\n the process";
}
echo $(GET_RECORDS);
As in the above, the actual running of the method may itself be wrapped in an echo which supersedes any echos that may be in the method itself. Obviously, I watered this down for brevity. It was not so easy to spot!
You can then inform your comrades that a better way to execute functions would be like so:
#!/bin/bash
function GET_RECORDS()
{
echo -e "starting\n the process";
}
GET_RECORDS;
Simply type
echo
to get a new line
POSIX 7 on echo
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html
-e is not defined and backslashes are implementation defined:
If the first operand is -n, or if any of the operands contain a <backslash> character, the results are implementation-defined.
unless you have an optional XSI extension.
So I recommend that you should use printf instead, which is well specified:
format operand shall be used as the format string described in XBD File Format Notation [...]
the File Format Notation:
\n <newline> Move the printing position to the start of the next line.
Also keep in mind that Ubuntu 15.10 and most distros implement echo both as:
a Bash built-in: help echo
a standalone executable: which echo
which can lead to some confusion.
str='hello\nworld'
$ echo | sed "i$str"
hello
world
You can also do:
echo "hello
world"
This works both inside a script and from the command line.
On the command line, press Shift+Enter to do the line break inside the string.
This works for me on my macOS and my Ubuntu 18.04 (Bionic Beaver) system.
For only the question asked (not special characters etc) changing only double quotes to single quotes.
echo -e 'Hello,\nWorld!'
Results in:
Hello,
World!
There is a new parameter expansion added in Bash 4.4 that interprets escape sequences:
${parameter#operator} - E operator
The expansion is a string that is the value of parameter with
backslash escape sequences expanded as with the $'…' quoting
mechanism.
$ foo='hello\nworld'
$ echo "${foo#E}"
hello
world
I just use echo without any arguments:
echo "Hello"
echo
echo "World"
To print a new line with echo, use:
echo
or
echo -e '\n'
This could better be done as
x="\n"
echo -ne $x
-e option will interpret backslahes for the escape sequence
-n option will remove the trailing newline in the output
PS: the command echo has an effect of always including a trailing newline in the output so -n is required to turn that thing off (and make it less confusing)
My script:
echo "WARNINGS: $warningsFound WARNINGS FOUND:\n$warningStrings
Output:
WARNING : 2 WARNINGS FOUND:\nWarning, found the following local orphaned signature file:
On my Bash script I was getting mad as you until I've just tried:
echo "WARNING : $warningsFound WARNINGS FOUND:
$warningStrings"
Just hit Enter where you want to insert that jump. The output now is:
WARNING : 2 WARNINGS FOUND:
Warning, found the following local orphaned signature file:
If you're writing scripts and will be echoing newlines as part of other messages several times, a nice cross-platform solution is to put a literal newline in a variable like so:
newline='
'
echo "first line${newline}second line"
echo "Error: example error message n${newline}${usage}" >&2 #requires usage to be defined
If the previous answers don't work, and there is a need to get a return value from their function:
function foo()
{
local v="Dimi";
local s="";
.....
s+="Some message here $v $1\n"
.....
echo $s
}
r=$(foo "my message");
echo -e $r;
Only this trick worked on a Linux system I was working on with this Bash version:
GNU bash, version 2.2.25(1)-release (x86_64-redhat-linux-gnu)
You could also use echo with braces,
$ (echo hello; echo world)
hello
world
This got me there....
outstuff=RESOURCE_GROUP=[$RESOURCE_GROUP]\\nAKS_CLUSTER_NAME=[$AKS_CLUSTER_NAME]\\nREGION_NAME=[$REGION_NAME]\\nVERSION=[$VERSION]\\nSUBNET-ID=[$SUBNET_ID]
printf $outstuff
Yields:
RESOURCE_GROUP=[akswork-rg]
AKS_CLUSTER_NAME=[aksworkshop-804]
REGION_NAME=[eastus]
VERSION=[1.16.7]
SUBNET-ID=[/subscriptions/{subidhere}/resourceGroups/makeakswork-rg/providers/Microsoft.Network/virtualNetworks/aks-vnet/subnets/aks-subnet]
Sometimes you can pass multiple strings separated by a space and it will be interpreted as \n.
For example when using a shell script for multi-line notifcations:
#!/bin/bash
notify-send 'notification success' 'another line' 'time now '`date +"%s"`
With jq:
$ jq -nr '"Hello,\nWorld"'
Hello,
World
Additional solution:
In cases, you have to echo a multiline of the long contents (such as code/ configurations)
For example:
A Bash script to generate codes/ configurations
echo -e,
printf might have some limitation
You can use some special char as a placeholder as a line break (such as ~) and replace it after the file was created using tr:
echo ${content} | tr '~' '\n' > $targetFile
It needs to invoke another program (tr) which should be fine, IMO.

Resources