Running programs only if they are installed, and ignoring them otherwise - shell

When writing shell scripts, is the an idiom or swift way to run a program only if it is installed, and if it is not, just let it be (or handle the error in some other way apart from installing it)?
More specifically, I have a lot of servers which I access over ssh, and whenever I get a new server, I simply copy all my rc-files to it. The .zshrc starts tmux unless it is already running. Some of the servers (not all) do not have tmux installed. I do not want to install it because of disk space limitations, I do not want to have different rc-files for different servers, and I do not want my rc-files to be interrupted when executing them.
I have seen solutions involving apt-cache policy <package-name>, so I guess I could use that and pipe it to something like grep -e 'Installed: (none)', but that would assume that the server is running Debian or Ubuntu, which I can not do, and it would only work for packages that were installed with apt, not things I have installed in other ways.

command -v <command> is the common (and POSIX) way to check if a command could be executed (is executable and on the $PATH).
E.g:
command -v tmux >/dev/null &&
tmux a -t name
(>/dev/null since, if the command exists, its path will be printed to STDOUT.)
It could be nice to put it in a reusable function:
maybe() {
! command -v "${1}" >/dev/null ||
"$#"
}
Then one could use:
maybe tmux a -t name
And if tmux is available then tmux a -t name will be run, otherwise it’ll be silently ignored.
Or, if you want some feedback when a command is not available:
maybe() {
if command -v "${1}" >/dev/null
then
"$#"
else
printf 'Command "%s" not available, skipping\n' "${1}" >&2
fi
}

This might help-
1) Assuming tmux is available in PATH (as it must be executable)
isAvailable=$(type -P tmux)
if [[ -x $isAvailable ]]; then
...
2) Verify file is present on a specific path (Copying all rc-files)
export FILEPATH="..."
if[[ -f $FILEPATH ]]; then

Related

Bash check if script is running with exact options

I know how to check if a script is already running (if pidof -o %PPID -x "scriptname.sh"; then...). But now I have a script that accepts inputs as flags, so it can be used in several different scenarios, many of which will probably run at the same time.
Example:
/opt/scripts/backup/tar.sh -d /directory1 -b /backup/dir -c /config/dir
and
/opt/scripts/backup/tar.sh -d /directory2 -b /backup/dir -c /config/dir
The above runs a backup script that I wrote, and the flags are the parameters for the script: the directory being backed up, the backup location, and the configuration location. The above example are two different backups (directory 1 and directory 2) and therefore should be allowed to run simultaneously.
Is there any way for a script to check if it is being run and check if the running version is using the exact same parameters/flags?
The ps -Af command will provide you all the processes that run on you os with the "command" line used to run them.
One solution :
if ps auxwww | grep '/[o]pt/scripts/backup/tar.*/directory2'; then
echo "running"
else
echo "NOT running"
fi

Adding printers by shell script; works in terminal but not as .command

I am trying to provide a clickable .command to set up printers in Macs for my workplace. I thought since it is something I do very frequently, I can write a shellscript for each printer and save it on a shared server. Then, when I need to add a printer for someone, I can just find the shell script on the server and execute it. My current command works in terminal, but once executed as a .command, it comes up with the errors.
This is my script:
#!/bin/sh
lpadmin -p ‘PRINTERNAME’ -D PRINTER\ NAME -L ‘OFFICE’ -v lpd://xx.xx.xx.xx -P /Library/Printers/PPDs/Contents/Resources/Xerox\ WorkCentre\ 7855.gz -o printer-is-shared=false -E​
I get this error after running the script:
lpadmin: Unknown option “?”.
I find this strange, because there is no "?" in the script.
I have a idea, why not try it like this ? there are huge differences between sh shells, so let me know if it rocks, I have more ideas.
#!/bin/sh
PPD="PRINTERNAME"
INFO="PRINTER\ NAME"
LOC="OFFICE"
URI="lpd://xx.xx.xx.xx"
OP ="printer-is-shared=false"
# This parameter P is new to me. Is it the paper-name ?
P="/Library/Printers/PPDs/Contents/Resources/Xerox\ WorkCentre\ 7855.gz"
lpadmin -p "$PPD" -D "$INFO" -L "$LOC" -v "$URI" -P "$P" -o "$OP" -E;

In this simple Docker wrapper script example, how may one correctly pass a current working directory path which contains spaces?

My Docker wrapper script works as intended when the current working directory does not contain spaces, however there is a bug when it does.
I have simplified an example to make use of the smallest official Docker image I could find and a well known GNU core utility. Of course this example is not very useful. In my real world use case, a much more complicated environment is packaged.
Docker Wrapper Script:
#!/usr/bin/env bash
##
## Dockerized ls
##
set -eux
# Only allocate tty if one is detected
# See https://stackoverflow.com/questions/911168/how-to-detect-if-my-shell-script-is-running-through-a-pipe
if [[ -t 0 ]]; then
DOCKER_RUN_OPTIONS+="-i "
fi
if [[ -t 1 ]]; then
DOCKER_RUN_OPTIONS+="-t "
fi
WORK_DIR="$(realpath .)"
DOCKER_RUN_OPTIONS+="--rm --user=$(id -u $(logname)):$(id -g $(logname)) --workdir=${WORK_DIR} --mount type=bind,source=${WORK_DIR},target=${WORK_DIR}"
exec docker run ${DOCKER_RUN_OPTIONS} busybox:latest ls "$#"
You can save this somewhere as /tmp/docker_ls for example. Remember to chmod +x /tmp/docker_ls
Now you are able to use this Dockerized ls in any path which contains no spaces as follows:
/tmp/docker_ls -lah
/tmp/docker_ls -lah | grep 'r'
Note that /tmp/docker_ls -lah /path/to/something is not implemented. The wrapper script would have to be adapted to parse parameters and mount the path argument into the container.
Can you see why this would not work when current working directory path contains spaces? What can be done to rectify it?
Solution:
#david-maze's answer solved the problem. Please see: https://stackoverflow.com/a/55763212/1782641
Using his advice I refactored my script as follows:
#!/usr/bin/env bash
##
## Dockerized ls
##
set -eux
# Only allocate tty if one is detected. See - https://stackoverflow.com/questions/911168
if [[ -t 0 ]]; then IT+=(-i); fi
if [[ -t 1 ]]; then IT+=(-t); fi
USER="$(id -u $(logname)):$(id -g $(logname))"
WORKDIR="$(realpath .)"
MOUNT="type=bind,source=${WORKDIR},target=${WORKDIR}"
exec docker run --rm "${IT[#]}" --user "${USER}" --workdir "${WORKDIR}" --mount "${MOUNT}" busybox:latest ls "$#"
If your goal is to run a process on the current host directory as the current host user, you will find it vastly easier and safer to use a host process, and not an isolation layer like Docker that intentionally tries to hide these things from you. For what you’re showing I would just skip Docker and run
#!/bin/sh
ls "$#"
Most software is fairly straightforward to install without Docker, either using a package manager like APT or filesystem-level isolation like Python’s virtual environments and Node’s node_modules directory. If you’re writing this script then Docker is just getting in your way.
In a portable shell script there’s no way to make “a list of words” in a way that keeps their individual wordiness. If you know you’ll always want to pass some troublesome options then this is still fairly straightforward: include them directly in the docker run command and don’t try to create a variable of options.
#!/bin/sh
RM_IT="--rm"
if [[ -t 0 ]]; then RM_IT="$RM_IT -i"; fi
if [[ -t 1 ]]; then RM_IT="$RM_IT -t"; fi
UID=$(id -u $(logname))
GID=$(id -g $(logname))
# We want the --rm -it options to be expanded into separate
# words; we want the volume options to stay as a single word
docker run $RM_IT "-u$UID:$GID" "-w$PWD" "-v$PWD:$PWD" \
busybox \
ls "$#"
Some shells like ksh, bash, and zsh have array types, but these shells may not be present on every system or environment (your busybox image doesn’t have any of these for example). You also might consider picking a higher-level scripting language that can more explicitly pass words into an exec type call.
I'm taking a stab at this to give you something to try:
Change this:
DOCKER_RUN_OPTIONS+="--rm --user=$(id -u $(logname)):$(id -g $(logname)) --workdir=${WORK_DIR} --mount type=bind,source=${WORK_DIR},target=${WORK_DIR}"
To this:
DOCKER_RUN_OPTIONS+="--rm --user=$(id -u $(logname)):$(id -g $(logname)) --workdir=${WORK_DIR} --mount type=bind,source='${WORK_DIR}',target='${WORK_DIR}'"
Essentially, we are putting the ' in there to escape the space when the $DOCKER_RUN_OPTIONS variable is evaluated by bash on the 'exec docker' command.
I haven't tried this - it's just a hunch / first shot.

path of running bash script found when re-runs as sudo

The following code checks if you have root authority, then runs the script again with it :
CMDLN_ARGS="$#" # Command line arguments for this script (if any)
export CMDLN_ARGS
func_check_for_sudo() {
if [ ! $( id -u ) -eq 0 ]; then
echo "You may be asked for your login password for [`whoami`]." ;sleep 1
LAUNCH="`dirname \"${0}\"`"
exec sudo -S su -c ${LAUNCH}/$(basename ${0}) ${CMDLN_ARGS}
exit ${?}
fi
}
Where things are going wrong is when I place this script in a "$HOME/bin" folder or something so I can just launch it without the path. It gives me the error "No such file or directory". I need the script to get that information and correctly pass it to exec.
My question is this: how do I get the /path/to/script_name from within a script correctly when it is called without the path? To recap, I'm calling MY_SCRIPT insead /path/to/MY_SCRIPT which breaks my script because it has to check for root authority and run again if you don't have it.
Basically the line of code in question is this where ${0} is the script name (with path if you called it with one):
exec sudo -S su -c ${0} ${CMDLN_ARGS}
There are a couple of problems here:
Finding the path to the script. There are a couple of easy ways to do this: use "$BASH_SOURCE" instead of $0; or simply take advantage of the fact that (at least by default), sudo preserves $PATH, so sudo "$0" ... will resolve the script fine.
The second is that the script doesn't preserve its arguments properly. Spaces within arguments will be mistaken for breaks between arguments, and wildcards will be erroneously expanded. This is because CMDLN_ARGS="$#" mushes all the arguments together separated by spaces, and then ${CMDLN_ARGS} re-splits on spaces (maybe not the same way) and also expands wildcards.
Here's my take at correcting the problems. Note that putting the handler in a function just adds a layer of unnecessary complication, so I just put it inline. I also used sudo's -p option to clean up the prompting slightly.
if [ $( id -u ) -ne 0 ]; then
exec sudo -p "Login password for %p: " "$0" "$#"
exit $?
fi

How to check for path expansion for executables

Like most makefiles, I have some bash scripts that use variables for executables. For example, $mysql_exec for /usr/bin/mysql. I would like to be able to say something like:
mysql_exec=mysql
to use $PATH or
mysql_exec=/usr/bin/mysql
to get an absolute location without $PATH. I also want to check to see if the executable is valid using something like this:
if [ ! -x $mysql_exec ] ...
However, this command:
if [ ! -x "mysql" ]; then print "oh oh"; fi
Actually prints "oh oh", even though mysql is in my $PATH. From the command-line, typing mysql has the same effect as typing /usr/bin/mysql. So, how do I get bash really check to see if $mysql_exec is an executable ($PATH and all)?
In Bash, you can use the built-in type -P to force a resolution against the PATH or type -p to show the path only if there's no alias or function by that name. Using type avoids calling an external such as which.
Something like this may do what you're looking for:
[ -x "$(type -p "$mysql_exec")" ]
whether you use
mysql_exec=mysql
or
mysql_exec=/usr/bin/mysql
which is a small program that checks $PATH for a program specified as an argument:
$ which mysql
/usr/bin/mysql
Another useful utility (that isn't installed on all systems but is included in GNU CoreUtils) is readlink. readlink can give you back a full and absolute path, without symlinks. For example:
$ cd ~me/bin
$ ln -s `which mysql` mysupersql
$ readlink -f mysupersql
/usr/bin/mysql
I often use a combination of both so I know that the path is both absolute and not a symlink:
mysql_exec=$(readlink -f `which mysql`)
if [ ! -x "$mysql_exec" ] ; then
...
To add to Kaleb's answer, you an also check if the file is a symlink using if [-L $file] and follow that symlink if you can't install readlink. The rest of the check though remains as the way Kaleb mentioned.

Resources