Bash Script has unexpected behavior if run with absolut path - bash

So we have this script that is supposed to change the IP of a linux machine based on user input. This user input has to be validated.
If the script is run inside the directory in which it lays, everything works as expected, but as soon as it's run with an absolute path, it seems to break on some points.
I already tried to use the debug option set -x but the output stays almost the same.
read -p "Please enter the netmask (CIDR format): " netmask
if [ ! $(echo "$netmask" | egrep "^([12]?[0-9]?)$") ];
then
subnetok=0
fi
if [ "$subnetok" == "0" ];
then
echo -e "\033[41m\033[39m Subnetmask is invalid!\033[0m"
sleep 1
return 1
fi
This is the debug output if the script is run inside the directory:
++ echo 24
++ egrep '^([12]?[0-9]?)$'
+ '[' '!' 24 ']'
+ '[' '' == 0 ']'
and this is the debug output if the script is run with an absolute path
+++ echo 24
+++ egrep --color=auto '^([12]?[0-9]?)$'
++ '[' '!' 24 ']'
++ '[' 0 == 0 ']'
++ echo -e 'Subnetmask is invalid'
I expect the output to be the same with the same numbers

When you run the script with this:
. /usr/local/script/script.sh
This uses the . command, which runs the script in the current shell (equivalent to source). That is, it runs it in your interactive shell rather than forking a new shell to run it. See: What is the difference between ./somescript.sh and . ./somescript.sh
This has (at least) two effects:
The current shell is interactive, and apparently has an alias defined for egrep, which makes things a little weird. Not really a problem, just weird.
The current shell apparently already has a definition for the variable subnetok, and it's "0". It's probably left over from a previous time you ran the script this way. This is what's causing the problem.
The primary solution is that the script needs to explicitly initialize subnetok rather than just assuming that it's undefined:
subnetok=1
if ...
Alternately, if you don't need the variable for anything else, you could just skip it and handle the condition immediately:
if [ ! $(echo "$netmask" | egrep "^([12]?[0-9]?)$") ]; # See below for alternatives
then
echo -e "\033[41m\033[39m Subnetmask is invalid!\033[0m"
...
Other recommendations:
Run the script without the .:
/usr/local/script/script.sh
Give the script a proper shebang line (if it doesn't already have one) that specifies the bash shell (i.e. #!/bin/bash or #!/usr/bin/env bash).
Use a better method to check the subnet for validity, like:
if ! echo "$netmask" | grep -Eq "^[12]?[0-9]?$"
or (bash only):
if ! [[ "$netmask" =~ ^[12]?[0-9]?$ ]]
Don't use echo -e, as it's not portable (even between different versions of the same OS, modes of the shell, etc). Use printf instead (and I'd recommend single-quotes for strings that contain backslashes, because in some cases they'll get pre-parsed when in double-quotes):
printf '\033[41m\033[39m Subnetmask is invalid!\033[0m'
Note that printf is not a drop-in replacement for echo -e, it's considerably more complicated when you're using variables and/or multiple arguments. Read the man page.

Comment:
By running with an absolute path I mean . /usr/local/script/script.sh instead of cd into /usr/local/script/ and then ./script.sh
The difference is that in one case you are executing the script and in another case you are sourcing the script. See What is the difference between executing a Bash script vs sourcing it? for more information.
When you are running ./script.sh without a space between the dot and the slash you are executing the script in a new shell. When you are running . /usr/local/script/script.sh you are sourcing the script in the current shell. This can have implications if you have for example an alias set in your current shell that would not be present in a new shell, such as alias egrep='egrep --color=auto'. That's why there is a difference.
From the linked question:
Both sourcing and executing the script will run the commands in the script line by line, as if you typed those commands by hand line by line.
The differences are:
When you execute the script you are opening a new shell, type the commands in the new shell, copy the output back to your current shell, then close the new shell. Any changes to environment will take effect only in the new shell and will be lost once the new shell is closed.
When you source the script you are typing the commands in your current shell. Any changes to the environment will take effect and stay in your current shell.
Use source if you want the script to change the environment in your currently running shell. use execute otherwise.

Related

Assigning a variable in a shell script for use outside of the script

I have a shell script that sets a variable. I can access it inside the script, but I can't outside of it. Is it possible to make the variable global?
Accessing the variable before it's created returns nothing, as expected:
$ echo $mac
$
Creating the script to create the variable:
#!/bin/bash
mac=$(cat \/sys\/class\/net\/eth0\/address)
echo $mac
exit 0
Running the script gives the current mac address, as expected:
$ ./mac.sh
12:34:56:ab:cd:ef
$
Accessing the variable after its created returns nothing, NOT expected:
$ echo $mac
$
Is there a way I can access this variable at the command line and in other scripts?
A child process can't affect the parent process like that.
You have to use the . (dot) command — or, if you like C shell notations, the source command — to read the script (hence . script or source script):
. ./mac.sh
source ./mac.sh
Or you generate the assignment on standard output and use eval $(script) to set the variable:
$ cat mac.sh
#!/bin/bash
echo mac=$(cat /sys/class/net/eth0/address)
$ bash mac.sh
mac=12:34:56:ab:cd:ef
$ eval $(bash mac.sh)
$ echo $mac
12:34:56:ab:cd:ef
$
Note that if you use no slashes in specifying the script for the dot or source command, then the shell searches for the script in the directories listed in $PATH. The script does not have to be executable; readable is sufficient (and being read-only is beneficial in that you can't run the script accidentally).
It's not clear what all the backslashes in the pathname were supposed to do other than confuse; they're unnecessary.
See ssh-agent for precedent in generating a script like that.

Why doesn't LIMIT=\`ulimit -u\` work in bash?

In my program I need to know the maximum number of process I can run. So I write a script. It works when I run it in shell but but when in program using system("./limit.sh"). I work in bash.
Here is my code:
#/bin/bash
LIMIT=\`ulimit -u\`
ACTIVE=\`ps -u | wc -l \`
echo $LIMIT > limit.txt
echo $ACTIVE >> limit.txt
Anyone can help?
Why The Original Fails
Command substitution syntax doesn't work if escaped. When you run:
LIMIT=\`ulimit -u\`
...what you're doing is running a command named
-u`
...with the environment variable named LIMIT containing the value
`ulimit
...and unless you actually have a command that starts with -u and contains a backtick in its name, this can be expected to fail.
This is because using backticks makes characters which would otherwise be syntax into literals, and running a command with one or more var=value pairs preceding it treats those pairs as variables to export in the environment for the duration of that single command.
Doing It Better
#!/bin/bash
limit=$(ulimit -u)
active=$(ps -u | wc -l)
printf '%s\n' "$limit" "$active" >limit.txt
Leave off the backticks.
Use modern $() command substitution syntax.
Avoid multiple redirections.
Avoid all-caps names for your own variables (these names are used for variables with meaning to the OS or system; lowercase names are reserved for application use).
Doing It Right
#!/bin/bash
exec >limit.txt # open limit.txt as output for the rest of the script
ulimit -u # run ulimit -u, inheriting that FD for output
ps -u | wc -l # run your pipeline, likewise with output to the existing FD
You have a typo on the very first line: #/bin/bash should be #!/bin/bash - this is often known as a "shebang" line, for "hash" (#) + "bang" (!)
Without that syntax written correctly, the script is run through the system's default shell, which will see that line as just a comment.
As pointed out in comments, that also means only the standardised options available to the builtin ulimit command, which doesn't include -u.

How can I easily log some specific command line commands into a file?

I often perform configuration changes using single line commands on Mac OS, Linux or even Windows and I want to easily log them in a file, so I can replay if I have to reconfigure the machine again.
Please not that I want to do these only for some commands, so the shell history is of not use.
Ideally I would like to be able to use some kind of shell extension that logs some of the commands.
As you know if you start your bash command with a space, this command is not logged into the history.
What if I can have another prefix that would do the opposite? Is there something there that can be used for this? A solution for bash would be more than enough and if there is an already existing solution it would much better than me writing a new one.
You could do your logging in PROMPT_COMMAND, extracting the specific commands from shell history and writing them to a file.
Something like:
log () {
last_command="$(history -p \!\!)"
if [[ $last_command == " "* ]] # save commands starting with *two* spaces
then
printf "%s\n" "$last_command" >> ~/special.log
fi
}
PROMPT_COMMAND="log; $PROMPT_COMMAND"
This has problems:
PROMPT_COMMAND is run each time the prompt is printed. Just pressing Enter multiple times could cause a command to be logged multiple times.
Marking with two spaces would, of course, need you to remove ignorespace or ignoreboth from HISTCONTROL so that commands starting spaces are logged at all.
AFAICT, history is updated when the next command is read, so the command is logged after the next command returns to the prompt, since that's when the correct history is available in PROMPT_COMMAND.
All this would be easier in zsh, with a preexec hook:
preexec () {
if [[ $1 == " "* ]]
then
printf "%s\n" "$1" >> ~/special.log
fi
}
The preexec function automatically gets the command as the first argument if history is enabled, saving us a deal of trouble. It is run when the command has been read, but before it begins execution, so the timing is perfect. From the documentation:
preexec
Executed just after a command has been read and is about to be
executed. If the history mechanism is active (regardless of whether
the line was discarded from the history buffer), the string that the
user typed is passed as the first argument, otherwise it is an empty
string. The actual command that will be executed (including expanded
aliases) is passed in two different forms: the second argument is a
single-line, size-limited version of the command (with things like
function bodies elided); the third argument contains the full text
that is being executed.
$ ls
$ echo foo | echo bar
bar
$ cat ~/special.log
ls
echo foo | echo bar
A function in .bashrc can be used like a prefix:
log_this_command () {
echo "$#" >> ~/a_log_file # log the command to file
"$#" # and run the command itself
}
Caveat: this only logs expanded arguments, rather than the raw input.
Source function with the same name function screencapture {echo "used parms: $#"; command screencapture $#}
appending to log file function screencapture {echo "$(date) screencapture " $# >> ~/log.txt; command screencapture $#}
as one runs screencapture command, log entry is created and command executes as uninterfered
you could automate in creating these functions, if the list of them is like .... all of them

How to recognize whether bash or dash is being used within a script?

I'm writing a bash script and it throws an error when using "sh" command in Ubuntu (it seems it's not compatible with dash, I'm learning on this subject). So I would like to detect if dash is being used instead of bash to throw an error.
How can I detect it in a script context?. Is it even possible?
You can check for the presence of shell-specific variables:
For instance, bash defines $BASH_VERSION.
Since that variable won't be defined while running in dash, you can use it to make the distinction:
[ -n "$BASH_VERSION" ] && isBash=1
Afterthought: If you wanted to avoid relying on variables (which, conceivably, could be set incorrectly), you could try to obtain the ultimate name of the shell executable running your script, by determining the invoking executable and, if it is a symlink, following it to its (ultimate) target.
The shell function getTrueShellExeName() below does that; for instance, it would return 'dash' on Ubuntu for a script run with sh (whether explicitly or via shebang #!/bin/sh), because sh is symlinked to dash there.
Note that the function's goal is twofold:
Be portable:
Work with all POSIX-compatible (Bourne-like) shells,
across at least most platforms, with respect to what utilities and options are used - see caveats below.
Work in all invocation scenarios:
sourced (whether from a login shell or not)
executed stand-alone, via the shebang line
executed by being passed as a filename argument to a shell executable
executed by having its contents piped via stdin to a shell executable
Caveats:
On at least one platform - macOS - sh is NOT a symlink, even though it is effectively bash. There, the function would return 'sh' in a script run with sh.
The function uses readlink, which, while not mandated by POSIX, is present on most modern platforms - though with differing syntax and features. Therefore, using GNU readlink's -f option to find a symlink's ultimate target is not an option.
(The only modern platform I'm personally aware of that does not have a readlink utility is HP-UX - see https://stackoverflow.com/a/24114056/45375 for a recursive-readlink implementation that should work on all POSIX platforms.)
The function uses the which utility (except in zsh, where it's a builtin), which, while not mandated by POSIX, is present on most modern platforms.
Ideally, ps -p $$ -o comm= would be sufficient to determine the path of the executable underlying the process, but that doesn't work as intended when directly executing shell scripts with shebang lines on Linux, at least when using the ps implementation from the procps-ng package, as found on Ubuntu, for instance: there, such scripts report the script's file name rather than the underlying script engine's.Tip of the hat to ferdymercury for his help.
Therefore, the content of special file /proc/$$/cmdline is parsed on Linux, whose first NUL-separated field contains the true executable path.
Example use of the function:
[ "$(getTrueShellExeName)" = 'bash' ] && isBash=1
Shell function getTrueShellExeName():
getTrueShellExeName() {
local trueExe nextTarget 2>/dev/null # ignore error in shells without `local`
# Determine the shell executable filename.
if [ -r /proc/$$/cmdline ]; then
trueExe=$(cut -d '' -f1 /proc/$$/cmdline) || return 1
else
trueExe=$(ps -p $$ -o comm=) || return 1
fi
# Strip a leading "-", as added e.g. by macOS for login shells.
[ "${trueExe#-}" = "$trueExe" ] || trueExe=${trueExe#-}
# Determine full executable path.
[ "${trueExe#/}" != "$trueExe" ] || trueExe=$([ -n "$ZSH_VERSION" ] && which -p "$trueExe" || which "$trueExe")
# If the executable is a symlink, resolve it to its *ultimate*
# target.
while nextTarget=$(readlink "$trueExe"); do trueExe=$nextTarget; done
# Output the executable name only.
printf '%s\n' "$(basename "$trueExe")"
}
Use $0 (that is the name of the executable of the shell being called).The command for example
echo $0
gives
/usr/bin/dash
for the dash and
/bin/bash
for a bash.The parameter substitution
${0##*/}
gives just 'dash' or 'bash'. This can be used in a test.
An alternative approach might be to test if a shell feature is available, for example to give an idea...
[[ 1 ]] 2>/dev/null && echo could be bash || echo not bash, maybe dash
echo $0 and [[ 1 ]] 2>/dev/null && echo
could be bash || echo not bash, maybe bash worked for me running Ubuntu 19.
Done slight Pascal, Fortran and C in school, but need to become fluent in shell script.

BASH Variables with multiple commands and reentrant

I have a bash script that sources contents from another file. The contents of the other file are commands I would like to execute and compare the return value. Some of the commands are have multiple commands separated by either a semicolon (;) or by ampersands (&&) and I can't seem to make this work. To work on this, I created some test scripts as shown:
test.conf is the file being sourced by test
Example-1 (this works), My output is 2 seconds in difference
test.conf
CMD[1]="date"
test.sh
. test.conf
i=2
echo "$(${CMD[$i]})"
sleep 2
echo "$(${CMD[$i]})"
Example-2 (this does not work)
test.conf (same script as above)
CMD[1]="date;date"
Example-3 (tried this, it does not work either)
test.conf (same script as above)
CMD[1]="date && date"
I don't want my variable, CMD, to be inside tick marks because then, the commands would be executed at time of invocation of the source and I see no way of re-evaluating the variable.
This script essentially calls CMD on pass-1 to check something, if on pass-1 I get a false reading, I do some work in the script to correct the false reading and re-execute & re-evaluate the output of CMD; pass-2.
Here is an example. Here I'm checking to see if SSHD is running. If it's not running when I evaluate CMD[1] on pass-1, I will start it and re-evaluate CMD[1] again.
test.conf
CMD[1]=`pgrep -u root -d , sshd 1>/dev/null; echo $?`
So if I modify this for my test script, then test.conf becomes:
NOTE: Tick marks are not showing up but it's the key below the ~ mark on my keyboard.
CMD[1]=`date;date` or `date && date`
My script looks like this (to handle the tick marks)
. test.conf
i=2
echo "${CMD[$i]}"
sleep 2
echo "${CMD[$i]}"
I get the same date/time printed twice despite the 2 second delay. As such, CMD is not getting re-evaluate.
First of all, you should never use backticks unless you need to be compatible with an old shell that doesn't support $() - and only then.
Secondly, I don't understand why you're setting CMD[1] but then calling CMD[$i] with i set to 2.
Anyway, this is one way (and it's similar to part of Barry's answer):
CMD[1]='$(date;date)' # no backticks (remember - they carry Lime disease)
eval echo "${CMD[1]}" # or $i instead of 1
From the couple of lines of your question, I would have expected some approach like this:
#!/bin/bash
while read -r line; do
# munge $line
if eval "$line"; then
# success
else
# fail
fi
done
Where you have backticks in the source, you'll have to escape them to avoid evaluating them too early. Also, backticks aren't the only way to evaluate code - there is eval, as shown above. Maybe it's eval that you were looking for?
For example, this line:
CMD[1]=`pgrep -u root -d , sshd 1>/dev/null; echo $?`
Ought probably look more like this:
CMD[1]='`pgrep -u root -d , sshd 1>/dev/null; echo $?`'

Resources