What does $? mean in a shell if statement? - shell

What is the meaning of the following statement in shell script?
if ($?REGRESS) then
....
endif
from where the given function is taking input from? This is a part of an old script which I am not able to understand.

From the csh the man page:
$?name
${?name}
Substitutes the string `1' if name is set, `0' if it is not.
But stop using csh (obligatory comment for future readers who may have somehow missed the memo that using csh is bad for you health).

$? is a special variable. It stores the exit status of last command. If the last command runs successfully then it will be 0 and some other value for failure.

Related

Use a variable on a script command line when its value isn't set until after the script starts

How to correctly pass to the script and substitute a variable that is already defined there?
My script test.sh:
#!/bin/bash
TARGETARCH=amd64
echo $1
When I enter:
bash test.sh https://example/$TARGETARCH
I want to see
https://example/amd64
but I actually see
https://example/
What am I doing wrong?
The first problem with the original approach is that the $TARGETARCH is removed by your calling shell before your script is ever invoked. To prevent that, you need to use quotes:
./yourscript 'https://example.com/$TARGETARCH'
The second problem is that parameter expansions only happen in code, not in data. This is, from a security perspective, a Very Good Thing -- if data were silently treated as code it would be impossible to write secure scripts handling untrusted data -- but it does mean you need to do some more work. The easy thing, in this case, is to export your variable and use GNU envsubst, as long as your operating system provides it:
#!/bin/bash
export TARGETARCH=amd64
substitutedValue=$(envsubst <<<"$1")
echo "Original value was: $1"
echo "Substituted value is: $substitutedValue"
See the above running in an online sandbox at https://replit.com/#CharlesDuffy2/EcstaticAfraidComputeranimation#replit.nix
Note the use of yourscript instead of test.sh here -- using .sh file extensions, especially for bash scripts as opposed to sh scripts, is an antipattern; the essay at https://www.talisman.org/~erlkonig/documents/commandname-extensions-considered-harmful/ has been linked by the #bash IRC channel on this topic for over a decade.
For similar reasons, changing bash yourscript to ./yourscript lets the #!/usr/bin/env bash line select an interpreter, so you aren't repeating the "bash" name in multiple places, leading to the risk of those places getting out of sync with each other.

Execute command that results from execution of a script whose name is in a variable

When posting this question originally, I totally misworded it, obtaining another, reasonable but different question, which was correctly answered here.
The following is the correct version of the question I originally wanted to ask.
In one of my Bash scripts, there's a point where I have a variable SCRIPT which contains the /path/to/an/exe which, when executed, outputs a line to be executed.
What my script ultimately needs to do, is executing that line to be executed. Therefore the last line of the script is
$($SCRIPT)
so that $SCRIPT is expanded to /path/to/an/exe, and $(/path/to/an/exe) executes the executable and gives back the line to be executed, which is then executed.
However, running shellcheck on the script generates this error:
In setscreens.sh line 7:
$($SCRIPT)
^--------^ SC2091: Remove surrounding $() to avoid executing output.
For more information:
https://www.shellcheck.net/wiki/SC2091 -- Remove surrounding $() to avoid e...
Is there a way I can rewrite that $($SCRIPT) in a more appropriate way? eval does not seem to be of much help here.
If the script outputs a shell command line to execute, the correct way to do that is:
eval "$("$SCRIPT")"
$($SCRIPT) would only happen to work if the command can be completely evaluated using nothing but word splitting and pathname expansion, which is generally a rare situation. If the program instead outputs e.g. grep "Hello World" or cmd > file.txt then you will need eval or equivalent.
You can make it simple by setting the command to be executed as a positional argument in your shell and execute it from the command line
set -- "$SCRIPT"
and now run the result that is obtained by expansion of SCRIPT, by doing below on command-line.
"$#"
This works in case your output from SCRIPT contains multiple words e.g. custom flags that needs to be run. Since this is run in your current interactive shell, ensure the command to be run is not vulnerable to code injection. You could take one step of caution and run your command within a sub-shell, to not let your parent environment be affected by doing ( "$#" ; )
Or use shellcheck disable=SCnnnn to disable the warning and take the occasion to comment on the explicit intention, rather than evade the detection by cloaking behind an intermediate variable or arguments array.
#!/usr/bin/env bash
# shellcheck disable=SC2091 # Intentional execution of the output
"$("$SCRIPT")"
By disabling shellcheck with a comment, it clarifies the intent and tells the questionable code is not an error, but an informed implementation design choice.
you can do it in 2 steps
command_from_SCRIPT=$($SCRIPT)
$command_from_SCRIPT
and it's clean in shellcheck

What is exclamation mark at beginning of line doing in this BASH snippet?

I understand history expansion in C-shell and BASH, but I don't understand why the exclamation mark on the line with the read command is being used in this snippet from some source I'm looking at:
#!/bin/bash
set -e
. "$(dirname "$0")/includes.sh"
! read -d '' SOME_VAR <<"EOT"
some ASCII art
EOT
echo -e "\033[95m$SOME_VAR\033[0m"
Why would you negate the return value of the read command (I think that is its effect, not history expansion)? Are there any possible error conditions, besides out-of-memory? The only one I can think of would be EINTR, which I think would be an abort condition (SIGINT or SIGHUP). And why would you double-quote the starting heredoc marker (EOT in this case), and not the ending marker?
It's from a major open source project, so I was guessing the author had some reason for doing this that I haven't been able to discern.
The set -e command tells bash to exit if a pipeline returns a non-zero exit status (basically if a command fails).
The ! negates the exit status, but it also inhibits the effect of set -e. The idea is that if a command is being executed as part of a condition, you don't want to terminate the shell. If it's preceded by !, the shell effectively assumes that it's being executed as a condition (even if the result is ignored).
Quoting the bash manual:
The shell does not exit if the command that fails is part of the
command list immediately following a 'while' or 'until' keyword, part
of the test in an 'if' statement, part of any command executed in a
'&&' or '||' list except the command following the final '&&' or '||',
any command in a pipeline but the last, or if the command's return
status is being inverted with '!'.
https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
(You're correct that the ! has nothing to do with history substitution in this context.)
The return code for the read built-in is
zero, unless end-of-file is encountered, read times out (in which case it's
greater than 128), a variable assignment error occurs,
or an invalid file descriptor is supplied as the argument to -u.
The relevant case here is end-of-file. Redirecting the input of read using <<EOT (and disabling normal termination using -d '') means that the read command will encounter an end-of-file, which would cause it to return a non-zero status. The ! prevents this from aborting the script (but the value is still assigned to $SOME_VAR).

Why doesn't this Bash script error out?

Here's my Bash script:
#!/bin/bash -e
if [ == "" ]; then
echo "BAD"
exit 1
fi
echo "OK"
And here's the output:
./test.sh: line 3: [: ==: unary operator expected
OK
The return code is 0.
There's an obvious syntax error at line 3. Rather than raise the syntax error and refuse to run the script, somehow the script just runs and reports the syntax error at run time. The -e flag hasn't protected me from this - apparently a syntax error in an if statement constitutes a false condition rather than a reason to immediately exit the program. BUT, somehow Bash has parsed that whole if ... fi block, so after ignoring the bad line, execution somehow resumes not at the next syntactically correct line but after the end of the block?
I have two questions:
What is going on?
How can I protect myself from this behaviour in future?
if runs the command [, and just examines its return code. Bash doesn't know nor care about the syntax for the [ command.
You can put some other command there, and Bash still won't know anything about its particular syntax.
Two things come to mind:
Using [[ instead of [: Bash does know and care about its syntax.
Using ShellCheck1; online, manually or within your favourite editor.
Both if and -e deal with exit codes: If it's non-zero if won't let you into the then block, and -e will exit. You can't really have both those behaviours at once. (Well, it seems [ exits with different codes for false results (1) and syntax errors (2), so it might be possible to ‘detect’ syntax errors.)
1Or some other tool, but that's the only one of which I know. Suggestions welcome.
You don't have a shell syntax error here.
You have an error in the arguments to the [ command/builtin.
The reason set -e doesn't help here is because that's explicitly not what it is supposed to do. set -e would become entirely useless if you couldn't have if statements in your code with it on. Just think about that.
If you look in the POSIX spec for what the -e/errexit flag does you see this description:
-e
When this option is on, when any command fails (for any of the reasons listed in Consequences of Shell Errors or by returning an exit status greater than zero), the shell immediately shall exit with the following exceptions:
The failure of any individual command in a multi-command pipeline shall not cause the shell to exit. Only the failure of the pipeline itself shall be considered.
The -e setting shall be ignored when executing the compound list following the while, until, if, or elif reserved word, a pipeline beginning with the ! reserved word, or any command of an AND-OR list other than the last.
If the exit status of a compound command other than a subshell command was the result of a failure while -e was being ignored, then -e shall not apply to this command.
This requirement applies to the shell environment and each subshell environment separately. For example, in:
set -e; (false; echo one) | cat; echo two
See point two there? That's your situation.
The reason the shell continues to execute is back to the "not a shell syntax error". You have a command error. The [ command/builtin attempted to parse its arguments and failed. It then returned an error return code. The if caught that, skipped its body and returned true (as per documented behavior of if when no conditions return true). So the shell script continued normally.
As I indicated in my comment, however, if you had used [[ (which is a bash-ism and a language construct) then your script would have had a syntax error and would have exited immediately on that line (at least in my tests).
From the bash man page
-e
Exit immediately if a pipeline (which may consist of a single simple command), a subshell command enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces (see SHELL GRAMMAR above) exits with a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test following the if or elif reserved words, part of any command executed in a && or ││ list except the command following the final && or ││, any command in a pipeline but the last, or if the command's return value is being inverted with !. A trap on ERR, if set, is executed before the shell exits. This option applies to the shell environment and each subshell environment separately (see COMMAND EXECUTION ENVIRONMENT above), and may cause subshells to exit before executing all the commands in the subshell.
Emphasis mine.
First, that isn't a syntax error. You are simply providing bad arguments to the [ command.
Second, the exit status of a command in the list following the if keyword are ignored for the purpose of the -e option.

Bash- passing input without shell interpreting parameter expansion chars

So I have a script where I type the script.sh followed by input for a set of if-else statements. Like this:
script.sh fnSw38h$?2
The output echoes out the input in the end.
But I noticed that $? is interpreted as 0/1 so the output would echo:
fnSw38h12
How can I stop the shell from expanding the characters and take it face value?
I looked at something like opt noglob or something similar but they didn't work.
When I put it like this:
script.sh 'fnSw38h$?2'
it works. But how do I capture that within single quotes ('') when I can't state variables inside it like Var='$1'
Please help!
How to pass a password to a script
I gather from the comments that the true purpose of this script is to validate a password. If this is an important or sensitive application, you really should be using professional security tools. If this application is not sensitive or this is just a learning exercise, then read on for a first introduction to the issues.
First, do not do this:
script.sh fnSw38h$?2
This password will appear in ps and be visible to any user on the system in plain text.
Instead, have the user type the password as input to the script, such as:
#!/bin/sh
IFS= read -r var
Here, read will gather input from the keyboard free from shell interference and it will not appear in ps output.
var will have the password for you to verify but you really shouldn't have plain text passwords saved anywhere for you to verify against. It is much better to put the password through a one-way hash and then compare the hash with something that you have saved in a file. For example:
var=$(head -n1 | md5sum)
Here, head will read one line (the password) and pass it to md5sum which will convert it to a hash. This hash can be compared with the known correct hash for this user's password. The text returned by head will be exactly what the user typed, unmangled by the shell.
Actually, for a known hash algorithm, it is possible to make a reverse look-up table for common passwords. So, the solution is to create a variable, called salt, that has some user dependent information:
var=$( { head -n1; echo "$salt"; } | md5sum)
The salt does not have to be kept secret. It is just there to make look-up tables more difficult to compute.
The md5sum algorithm, however, has been found to have some weaknesses. So, it should be replaced with more recent hash algorithms. As I write, that would probably be a sha-2 variant.
Again, if this is a sensitive application, do not use home-made tools
Answer to original question
how do I capture that within single quotes ('') when I can't state variables inside it like Var='$1'
The answer is that you don't need to. Consider, for example, this script:
#!/bin/sh
var=$1
echo $var
First, note that $$ and $? are both shell variables:
$ echo $$ $?
28712 0
Now, let's try our script:
$ bash ./script.sh '$$ $?'
$$ $?
These variables were not expanded because (1) when they appeared on the command line, they were in single-quotes, and (2) in the script, they were assigned to variables and bash does not expand variables recursively. In other words, on the line echo $var, bash will expand $var to get $$ $? but there it stops. It does not expand what was in var.
You can escape any dollar signs in a double-quoted string that are not meant to introduce a parameter expansion.
var=foo
# Pass the literal string fnSw38h$?2foo to script.sh
script.sh "fnSw38h\$?2$var"
You cannot do what you are trying to do. What is entered on the command line (such as the arguments to your script) must be in shell syntax, and will be interpreted by the shell (according to the shell's rules) before being handed to your script.
When someone runs the command script.sh fnSw38h$?2, the shell parses the argument as the text "fnSw38h", followed by $? which means "substitute the exit status of the last command here", followed by "2". So the shell does as it's been told, it substitutes the exit status of the last command, then hands the result of that to your script.
Your script never receives "fnSw38h$?2", and cannot recover the argument in that form. It receives something like "fnSw38h02" or "fnSw38h12", because that's what the user asked the shell to pass it. That might not be what the user wanted to pass it, but as I said, the command must be in shell syntax, and in shell syntax an unescaped and unnquoted $? means "substitute the last exit status here".
If the user wants to pass "$?" as part of the argument, they must escape or single-quote it on the command line. Period.

Resources