Making shell scripts robust against location they're called from - linux

What is the best way to write a shell script that will access files relative to it such that it doesn't matter where I call it from? "Easy" means the easiest/recommended way that will work across different systems/shells.
Example
Say I have a folder ~/MyProject with subfolders scripts/ and files/. In scripts/, I have a shell script foo.sh that wants to access files in files/:
if [ -f "../files/somefile.ext" ]; then
echo "File found"
else
echo "File not found"
fi
It'll work fine If I do cd ~/MyProject/scripts && ./foo.sh, but it will fail with cd ~/MyProject && scripts/foo.sh.

You can usually do:
mydir="$(dirname $0)"
to get the directory of the running script.
Then just use that to locate your files.

Make all paths absolute.
Use environmental variables when possible, for example $HOME.
You are running into a weakness of UNIX scripting.
Using a .profile or .bash_profile, your developers could set a bunch of ENV variables. Then your scripts could make use of those variables.
In one of the places I worked, you could run a script, that would prompt you for which version you wanted to look at, and set the ENVs such that developer interaction was pretty seamless.

Not sure if it's easy enough for you but this could give you a solution:
Shell Script Loader

if [ -f "../files/somefile.ext"]
then
echo "file was found"
else
echo "file was not found"
fi
second:
if [ ! -f /tmp/foo.txt ]; then
echo "File not found!"
fi

Unfortunately there doesn't seem to be a truly generic solution. My personal recommendation (and practice) is to write only for shells that provide consistent access to this--recent ksh93's ${.sh.file}, bash's $BASH_SOURCE, etc. (I don't know the zsh solution, but I'm sure there is one.)
Beyond that, the best solution is to avoid the problem in some way; e.g., for your example of a script in a git repository, you could require that the script be called from the directory it's in. After validating that by checking [[ -e myscript ]], you can then expect that relative links will work as expected. (Yes, for full robustness, you'll need to hardcode the basename of the script into the test, for the same reason this problem exists in the first place--it's available to the shell in all conceivable circumstances.)

Considering that $0 should contain the path to the executed script, you can simply cd to it and then process normally
So you simply do
scriptDir=$(dirname -- "$0")
cd -- "$scriptDir"
But that is still a hack, you probably should think of a way to work with absolute paths

Related

Bash script that would give out information about the desired directory

I need a script that would be similar to the ls and dir commands. To display information about the needed directory.
#!/bin/bash
for entry in *
do
echo "$entry"
done
But this script outputs files only in the directory where the script is located. How do I make the output in the directory that I need?
you can accept args with $1, etc. and refer to them later
for file in "$1"; do
This APP provides a better way to learn and understand unix/linux. I would like to suggest you to take a look.
https://play.google.com/store/apps/details?id=com.kanha.unixlinuxpocketbook
#!/bin/bash
for entry in /dir/I-need/*
do
echo "$entry"
done
also may be you would like
find /dir/I-need

Linux - How to access/execute file from another folder and not changing folder

I tried multiple scenarios but these things are more of a headace. I tried writing this bash also for the same, but need to change in the same file which i need to execute. Can someone help me with the same.
#!/bin/bash -p
if [ $# -lt 1 ]; then
echo "No arguments provided"
exit 0
fi
CURRENT_DIR=`pwd`
EXEC_DIR=`dirname "$1"`
FILENAME="`basename $1`"
cd $EXEC_DIR
CMD="./$FILENAME ${#:2}"
$CMD
cd $CURRENT_DIR
i need some two liner to work with the same file which i need to execute.
If your script is present in /opt/ and you are in /opt/dir/ then you can run the script from there only like this --> ../script.sh it will run the script from previous folder and run it in present folder
The easiest way is to use the full path of the file/script you want to execute :
/path/to/file/example.bash
You will stay in your current directory, but your script example.bash will be executed.

Identifying where to export PATH updates in both Mac OSX and Linux, regardless of terminal

I'm familiar with methods that rely on .bashrc across both platforms, but then there's always folks that have ZSH (.zshrc), and I'm wondering what the best way to check / identify the place to update a PATH variable permanently is across OSX and Linux, specifically from a Bash script, for at least these two types of terminals. Not sure if I need to do some sort of nested IF THENS, or if something is out there that is kind of elegant. Thanks.
There's a system wide profile at /etc/profile that you can use to set variables. Of course, you need root permission to do it.
This will work for bash users and zshell users, with the example in this case of adding miniconda, but you can change the value for DIRECTORY_TO_APPEND_TO_PATH pretty easily...
update_script_startup_file() {
DIRECTORY_TO_APPEND_TO_PATH="\$HOME/miniconda/bin"
echo "if [[ \":\$PATH:\" != *\":$DIRECTORY_TO_APPEND_TO_PATH:\"* ]]; then" >> $STARTUP_FILE
echo " export PATH=\"\$PATH:$DIRECTORY_TO_APPEND_TO_PATH\"" >> $STARTUP_FILE
echo "fi" >> $STARTUP_FILE
}
if [ -n "`$SHELL -c 'echo $BASH_VERSION'`" ]; then
STARTUP_FILE="$HOME/.bashrc"
update_script_startup_file
elif [ -n "`$SHELL -c 'echo $ZSH_VERSION'`" ]; then
STARTUP_FILE="$HOME/.zshrc"
update_script_startup_file
else
echo "Couldn't automatically add Miniconda to the PATH of your preferred terminal. We suggest working from Bash or ZShell."
fi
If anyone wants to edit this to add support for other shells, they're most welcome to!

Bash: Only allow script to run by being called from another script

We have two bash scripts to start up an application. The first (Start-App.sh) one sets up the environment and the second (startup.sh) is from a 3rd party that we are trying not to heavily edit. If someone runs the second script before the first the application does not come up correctly.
Is there a way to ensure that the startup.sh can only be called from the Start-App.sh script?
They are both in the same directory and run via bash on Red Hat Linux.
Is there a way to ensure that the startup.sh can only be called from the Start-App.sh script?
Ensure? No. And even less so without editing startup.sh at all. But you can get fairly close.
Below are three suggestions − you can either use one of them, or any combination of them.
The simplest, and probably the best, way is to add a single line at the top of startup.sh:
[ -z $CALLED_FROM_START_APP ] && { echo "Not called from Start-App.sh"; exit 42; }
And then call it from Start-App.sh like so:
export CALLED_FROM_START_APP=yes
sh startup.sh
of course, you can set this environment variable from the shell yourself, so it won't actually ensure anything, but I hope your engineering staff is mature enough not to do this.
You can also remove the execute permissions from startup.sh:
$ chmod a-x startup.sh
This will not prevent people from using sh startup.sh, so there is a very small guarantee here; but it might prevent auto-completion oopsies, and it will mark the file as "not intended to be executed" − if I see a directory with only one executable .sh file, I'll try and run that one, and not one of the others.
Lastly, you could perhaps rename the startup.sh script; for example, you could rename it to do_not_run, or "hide" it by renaming it to .startup. This probably won't interfere with the operation of this script (although I can't check this).
TL;DR:
[ $(basename "$0") = "Start-App.sh" ] || exit
Explanation
As with all other solutions presented it's not 100% bulletproof but this covers most common instances I've come across for preventing accidentally running a script directly as opposed to calling it from another script.
Unlike other approaches presented, this approach:
doesn't rely on manually set file names for each included/sourced script (i.e. is resilient to file name changes)
behaves consistently across all major *nix distros that ship with bash
introduces no unnecessary environment variables
isn't tied to a single parent script
prevents running the script through calling bash explicitly (e.g. bash myscript.sh)
The basic idea is having something like this at the top of your script:
[ $(basename "$0") = $(basename "$BASH_SOURCE") ] && exit
$0 returns the name of the script at the beginning of the execution chain
$BASH_SOURCE will always point to the file the currently executing code resides in (or empty if no file e.g. piping text directly to bash)
basename returns only the main file name without any directory information (e.g. basename "/user/foo/example.sh" will return example.sh). This is important so you don't get false negatives from comparing example.sh and ./example.sh for example.
To adapt this to only allow running when sourced from one specific file as in your question and provide a helpful error message to the end user, you could use:
[ $(basename "$0") = "Start-App.sh" ] || echo "[ERROR] To start MyApplication please run ./Start-App.sh" && exit
As mentioned from the start of the answer, this is not intended as a serious security measure of any kind, but I'm guessing that's not what you're looking for anyway.
You can make startup.sh non-executable by typing chmod -x startup.sh. That way the user would not be able to run it simply by typing ./startup.sh.
Then from Start-App.sh, call your script by explicitly invoking the shell:
sh ./startup.sh arg1 arg2 ...
or
bash ./startup.sh arg1 arg2 ...
You can check which shell it's supposed to run in by inspecting the first line of startup.sh, it should look like:
#!/bin/bash
You can set environment variable in your first script and before running second script check if that environment variable is set properly.
Another alternative is checking the parent process and finding the calling script. This also needs adding some code to the second script.
For example, in the called script, you can check the exit status of this and terminate.
ps $PPID | tail -1 | awk '$NF!~/parent/{exit 1}'
As others have pointed out, the short answer is "no", although you can play with permissions all day but this is still not bulletproof. Since you said you don't mind editing (just not heavily editing) the second script, the best way to accomplish this would be something along the lines of:
1) in the parent/first script, export an environment variable with its PID. This becomes the parent PID. For example,
# bash store parent pid
export FIRST_SCRIPT_PID = $$
2) then very briefly, in the second script, check to see if the calling PID matches the known acceptable parent PID. For example,
# confirm calling pid
if [ $PPID != $FIRST_SCRIPT_PID ] ; then
exit 0
fi
Check out these links here and here for reference.
To recap: the most direct way to do this is adding at least a minimal line or two to the second script, which hopefully doesn't count as "heavily editing".
You can create a script, let's call it check-if-my-env-set containing
#! /bin/bash
source Start-App.sh
exec /bin/bash $#
and replace the shebang (see this) on startup.sh by that script
#! /abs/path/to/check-if-my-env-set
#! /bin/bash
...
then, every time you run startup.sh it will ensure the environment is set correctly.
To the best of my knowledge, there is no way to do this in a way that it would be impossible to get around it.
However, you could stop most attempts by using permissions.
Change the owner of the startup.sh file:
sudo chown app_specific_user startup.sh
Make startup.sh only executable by the owner:
chmod u+x startup.sh
Run startup.sh as the app_specific_user from Start-App.sh:
sudo -u app_specific_user ./startup.sh

How to modify a file name within a shell script?

I am writing a shell script to sync to a github repo, kick off the build, then take the output file, rename it, and move it to a location where it can be seen by Apache.
It's the renaming of the file that I've got not the faintest how to do within a shell script (I have virtually no experience with shell scripts - my understanding
Compiler will create /var/espbuild/firstpart_1vXX_secondpart.bin
I need to move this file to:
/var/www/html/builds/espbuild/firstpart_1vXX_DATE_secondpart_postfix.bin
1vXX is the version number
DATE is the output of date +%m-%d
postfix is just a string.
I'm not really certain where to start for something like this - I'm sure there's a graceful way, since this is the kind of thing shell scripts are made for, but I know just about nothing about shell scripts.
Thanks in advance
You can get the result of a command into a variable by using $():
DATE=$(date +%m-%d)
Then just use it in the new filename:
INPUT=/var/espbuild/firstpart_1vXX_secondpart.bin
OUTPUT=/var/www/html/builds/espbuild/firstpart_1vXX_${DATE}_secondpart_postfix.bin
mv ${INPUT} ${OUTPUT}
Edit: To get out the version part, here's a quick example:
VERSION=$(grep -o 1v.. <<< ${INPUT})
Then OUTPUT should be set like:
OUTPUT=/var/www/html/builds/espbuild/firstpart_${VERSION}_${DATE}_secondpart_postfix.bin
You can use this in BASH:
f='/var/espbuild/firstpart_1vXX_secondpart.bin'
s="${f##*/}"
s2=${s##*_}
dest="/var/www/html/builds/espbuild/${s%_*}_$(date '+%m-%d')_${s2%.*}_postfix.bin"
echo "$dest"
/var/www/html/builds/espbuild/firstpart_1vXX_07-14_secondpart_postfix.bin
cp "$f" "$dest"

Resources