How to detect OS and load ZSH settings conditionally? - linux

I use several different OS's at home and work and I want to be able to load platorm-specific ZSH settings conditionally, depending on which OS I'm using at the given moment.
I tried this but it doesn't load everything I expect:
# Condtitional loading of zsh settings per platform
if command apt > /dev/null; then
source $ZSH_CUSTOM/os/debian.zsh
elif command systemctl > /dev/null; then
source $ZSH_CUSTOM/os/systemd.zsh
elif command freebsd-version > /dev/null; then
source $ZSH_CUSTOM/os/freebsd.zsh
elif [[ `uname` == "Darwin" ]]; then
source $ZSH_CUSTOM/os/mac.zsh
elif command kubectl > /dev/null; then
source $ZSH_CUSTOM/os/kubernetes.zsh
else
echo 'Unknown OS!'
fi
What is the best way to do this detection and what I'm doing wrong?
I know this approach of mine doesn't work as when I run zsh -o SOURCE_TRACE, it doesn't show all desired files sourced.
Thanks in advance!

Revised Answer (2020-Feb-09)
Thanks to #Cyberbeni for reminding me that apt on macOS would incorrectly match the system Java runtime's Annotation Processing Tool. Rolling up the necessary changes, we now have:
# What OS are we running?
if [[ $(uname) == "Darwin" ]]; then
source "$ZSH_CUSTOM"/os/mac.zsh
elif command -v freebsd-version > /dev/null; then
source "$ZSH_CUSTOM"/os/freebsd.zsh
elif command -v apt > /dev/null; then
source "$ZSH_CUSTOM"/os/debian.zsh
else
echo 'Unknown OS!'
fi
# Do we have systemd on board?
if command -v systemctl > /dev/null; then
source "$ZSH_CUSTOM"/os/systemd.zsh
fi
# Ditto Kubernetes?
if command -v kubectl > /dev/null; then
source "$ZSH_CUSTOM"/os/kubernetes.zsh
fi
Original answer
I answered exactly the same question on Reddit here, so to close the loop, here's what I wrote:
Your current logic literally says that, for instance, a Debian system cannot possibly run systemd or Kubernetes, which is clearly untrue. That's exactly what if...elif...else...fi implements: mutual exclusivity.
It looks to me like only the OS-specific tests need to be mutually exclusive, so you're probably looking at something like:
# What OS are we running?
if command apt > /dev/null; then
source $ZSH_CUSTOM/os/debian.zsh
elif command freebsd-version > /dev/null; then
source $ZSH_CUSTOM/os/freebsd.zsh
elif [[ `uname` == "Darwin" ]]; then
source $ZSH_CUSTOM/os/mac.zsh
else
echo 'Unknown OS!'
fi
# Do we have systemd on board?
if command systemctl > /dev/null; then
source $ZSH_CUSTOM/os/systemd.zsh
fi
# Ditto Kubernetes?
if command kubectl > /dev/null; then
source $ZSH_CUSTOM/os/kubernetes.zsh
fi
UPDATE: Actually, I didn't look closely enough at your code, and you're also calling command wrong. All your invocations should be of the form:
if command -v <cmd_name> > /dev/null
which returns success if <cmd_name> is found in your PATH. command <cmd_name> actually runs <cmd_name> and returns its exit status, which can return a failure exit code (i.e. false negative) due to lack of appropriate arguments.

Verifying an OS is system dependent. You can use the package managers to verify a certain distribution, but this not desired, since there are certainly other distributions which the same package manager as well.
You can try to use lsb_release and grep on the correct distribution. Or use uname.
Which OS'es do not load in your script, and which do?
Also, take a look here

Related

Why does this bash command's return value change?

What I actually want to do
Save a command's output and check its return status.
The solution?
After some googling I found basically the same answer here on StackOverflow as well as on AskUbuntu and Unix/Linux StackExchange:
if output=$(command); then
echo "success: $output"
fi
Problem
When trying out this solution with command info put the if clause is executed even if the actual command fails, but I can't explain myself why?
I tried to check the return value $? manually and it seems like the var= changes the return value:
$ info put
info: No menu item 'put' in node '(dir)Top'
$ echo $?
1
$ command info put
info: No menu item 'put' in node '(dir)Top'
$ echo $?
1
$ var=$(command info put)
info: No menu item 'put' in node '(dir)Top'
$ echo $?
0
$ var=$(command info put); echo $?
info: No menu item 'put' in node '(dir)Top'
0
It's also the same behavior when `
So why does that general solution not work in this case?
And how to change/adapt the solution to make it work properly?
My environment/system
I'm working on Windows 10 with WSL2 Ubuntu 20.04.2 LTS:
$ tmux -V
tmux 3.0a
$ echo $SHELL
/bin/bash
$ /bin/bash --version
GNU bash, version 5.0.17(1)-release (x86_64-pc-linux-gnu)
$ info --version
info (GNU texinfo) 6.7
When trying out this solution with command info put the if clause is executed even if the actual command fails, but I can't explain myself why?
Indeed, info exits with 0, when output is not a terminal and there's an error.
// texinfo/info.c
if ((!isatty (fileno (stdout))) && (user_output_filename == NULL))
{
user_output_filename = xstrdup ("-");
...
}
...
// in called get_initial_file()
asprintf (error, _("No menu item '%s' in node '%s'"),
(*argv)[0], "(dir)Top");
...
if (user_output_filename)
{
if (error)
info_error ("%s", error);
...
exit (0); // exits with 0!
}
References: https://github.com/debian-tex/texinfo/blob/master/info/info.c#L848 , https://github.com/debian-tex/texinfo/blob/master/info/info.c#L277 , https://github.com/debian-tex/texinfo/blob/master/info/info.c#L1066 .
why does that general solution not work in this case?
Because the behavior of the command changes when its output is redirected not to a terminal.
how to change/adapt the solution to make it work properly?
You could simulate a tty - https://unix.stackexchange.com/questions/157458/make-program-in-a-pipe-think-it-has-tty , https://unix.stackexchange.com/questions/249723/how-to-trick-a-command-into-thinking-its-output-is-going-to-a-terminal .
You could grab stderr of the command and check if it's not-empty or match with some regex.
I think you could also contact texinfo developers and let them know that it's I think a bug and make a patch, so it would be like exit(error ? EXIT_FAILURE : EXIT_SUCCESS);.
Instead of checking the exit status of the command, I ended up with saving the output and simply checking if there is any output that could be used for further processing (in my case piping into less):
my_less () {
output=$("$#")
if [[ ! -z "$output" ]]; then
printf '%s' "$output" | less
fi
}
Even with the bug in info, my function now works as the bug only affects the command's exit status. It's error messages are written to stderr as expected, so I'm using that behavior.

Extension .exe needed for WSL! How to write a generic script? (WSL, Cygwin, Linux, MacOS)

I use docopts in my Shell scripts. That works nicely from Cygwin.
I just need to be sure that docopts is present, at the top of my scripts:
command -v docopts > /dev/null 2>&1 || { echo >&2 "docopts not found"; exit 2; }
...
parsed="$(docopts -h "$help" -V "$version" : "$#")"
eval "$parsed"
But, in WSL, it needs the extension .exe to find the program to launch.
Should I adapt all my scripts this way?
DOCOPTS=
command -v docopts > /dev/null 2>&1 && DOCOPTS=docopts
command -v docopts.exe > /dev/null 2>&1 && DOCOPTS=docopts.exe
[ -z "$DOCOPTS" ] && { echo >&2 "docopts not found"; exit 2; }
...
parsed="$($DOCOPTS -h "$help" -V "$version" : "$#")"
eval "$parsed"
Or is there a much smarter way to do that, so that my scripts will work in any environment?
My recommendation is to install docopts in WSL rather than attempting to use the Cygwin docopts.exe version. That will (a) allow you to use the same config (without an .exe extension) in both, and (b) likely be more compatible. I've noticed and heard of a few idiosyncrasies when attempting to use Cygwin executables inside of WSL. WSL does a great job of providing the compatibility layer between Linux and Windows EXE, but Cygwin does some "magic" that might cause issues.
This looks good. Suggestion would be to have it configured based on the "uname".If uname is "cywgin" then DOCOPTS= docopts or docopts.exe based on WSL.This would be easier to maintain and would be readable.
Regards

Most efficient if statement in .zshrc to check whether Linux OS is running on WSL?

In my .zshrc file I conditionally set my PATH variable depending on whether I'm running on Linux or macOS - I'm now trying to figure out if there's a way I can efficiently detect from my .zshrc if I'm working on Linux running on WSL.
I'm wondering if I can somehow check for the existence of /mnt/c/Program Files or similar - but figure there must be a better way?
Example of my current .zshrc:
PATH="/usr/local/sbin:$PATH"
if ! [[ "$OSTYPE" == "darwin"* ]]; then
export PATH="$HOME/.nodenv/bin:$HOME/.rbenv/bin:$PATH"
fi
eval "$(rbenv init -)"
eval "$(nodenv init -)"
PATH="$HOME/.bin:$PATH"
if [[ "$OSTYPE" == "darwin"* ]]; then
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
export PATH="$PATH:$ANDROID_SDK_ROOT/tools:$ANDROID_SDK_ROOT/tools/bin:$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/build-tools:$ANDROID_SDK_ROOT/tools/lib/x86_64"
export PATH="$PATH:/usr/local/share/dotnet"
fi
If anyone has any better ideas than somehow checking for the existence of /mnt/c/Program Files I'd very much appreciate it!
There are many possible way to check WSL in any shell. Most reliable ways are:
From uname -r command output.
From /proc/version file.
From /proc/sys/kernel/osrelease file.
#!/bin/bash
if uname -r |grep -q 'Microsoft' ; then
echo True
fi
if grep -q -i 'Microsoft' /proc/version ; then
echo True
fi
if grep -q -i 'Microsoft' /proc/sys/kernel/osrelease ; then
echo True
fi
Also there are many file existence can be checked with shell script. For example, only WSL has 1. /dev/lxss 2. /bin/wslpath 3. /sbin/mount.drvfs 4. /proc/sys/fs/binfmt_misc/WSLInterop 5. /etc/wsl.conf files but GNU/Linux distributions has not.
See more:
screenFetch
netfetch
In WSL, there is a special file for checking interoperability called /proc/sys/fs/binfmt_misc/WSLInterop which is WSL specific file. You can check using the following command:
#!/bin/bash
if [ -f /proc/sys/fs/binfmt_misc/WSLInterop ]; then
echo True
fi
or more simple one-line code(in bash):
[ -f /proc/sys/fs/binfmt_misc/WSLInterop ]
This will return exit code 0 if true, exit code 1 if false.
Thanks to Biswapiryo's comment - I came up with this solution to detect WSL:
if [[ $(uname -r)] == ^*Microsoft$ ]]; then
# Code goes here
fi
Short/current answer:
To detect either WSL1 or WSL2, you can use a modified version of #MichaelSmith's answer:
#!/bin/zsh
if [[ $(uname -r) == (#s)*[mM]icrosoft*(#e) ]]; then
echo test
fi
More detail:
When this question was originally asked, only WSL1 existed, and uname -r would return something like:
4.4.0-22000-Microsoft
This is not a "real" kernel in WSL1, but just the number/name that Microsoft chooses to provide in response to that particular syscall. The 22000, in this case, is the Windows build number, which currently corresponds to the WSL release. Note that this is the case even in the current WSL Preview in the Microsoft Store, even though it is decoupled from the Windows release.
With WSL2, however, Microsoft provides a real Linux kernel, which returns something like:
5.10.102.1-microsoft-standard-WSL2
Earlier versions may have left off the -WSL2 portion.
Of course, if you build your own WSL2 kernel, you should update the test to match the kernel name you provide.

How to - multiple dropbox instances in Linux?

After looking around online it seems pretty easy to have multiple dropbox accounts running. All you have to do is change an environmental variable and then run dropbox. However, I've tried editing the .desktop file (see .desktop file specification) so the Exec line is changed from this:
Exec=dropbox start -i
which is the default, to this:
Exec=env "HOME\=/home/reg/.dropbox-alt" dropbox start -i
which from everything I have read should work. I've also tried all the variations of escaping and quoting like:
Exec=env HOME\=/home/reg/.dropbox-alt dropbox start -i
Exec=env "HOME=/home/reg/.dropbox-alt" dropbox start -i
Exec=env HOME=/home/reg/.dropbox-alt dropbox start -i
and nothing seems to launch dropbox. However if I try the same line in bash it tries to launch but falls short but that's only because dropbox is looking for a GUI. That being the case I would have thought that doing the above in the .desktop file would work but I get nothing at all happening.
I'm doing this without any dropbox instances running already so it cannot be that dropbox is looking for other instances and stopping itself from loading another instance.
If I try this in the .desktop file:
Exec=env dropbox start -i
It will launch dropbox but now it's the default instance which has no benefit.
Can anyone tell me what I'm missing to make this work?
Open a terminal and paste the following commands:
$ mkdir "$HOME"/.dropbox-alt
$ ln -s "$HOME/.Xauthority" "$HOME/.dropbox-alt/"
$ HOME="$HOME/.dropbox-alt"
$ /home/$USER/.dropbox-dist/dropboxd
Dropbox setup wizard window will appear. Finish the setup similarly as described in Method -1
start Dropbox from terminal
$ /home/$USER/.dropbox-dist/dropboxd
start Alternate-Dropbox from terminal
$ HOME="$HOME/.dropbox-alt" && /home/$USER/.dropbox-dist/dropboxd
Note:
You can create a small script with the above commands to start Dropbox.
One can put the script at startup. Don't forget to give the script execution permission.
chmod +x /path/to/script
I have tested the second method. Hope it will be useful.
#!/bin/bash
HOME_DIR=$HOME
DROPBOXES=("$HOME/.dropboxes/personal" "$HOME/.dropboxes/business")
function start_dropbox() {
HOME=$HOME_DIR
local flag
local home_dir
local OPTIND;
local verbose=0
local wait=0
while getopts p:vw opt; do
case $opt in
p) home_dir="$(echo $OPTARG | sed 's:/*$::')/" ;;
v) verbose=1 ;;
w) wait=1 ;;
*) ;;
esac
done
shift $((OPTIND-1))
# Test if the process is already running
local pid=$(ps aux|grep "${home_dir}.dropbox-dist"|grep -v 'grep'|tr -s ' '| cut -d' ' -f 2)
if [ -n "$pid" ]
then
echo "Process already running with home dir. of: $home_dir"
return 8 # Process already running
fi
# Create home directory if it doesn't exist
if [ ! -e "$home_dir" ]
then
if mkdir -p "$home_dir";
then
echo "Created directory: $home_dir"
else
echo "Failed to create directory: $home_dir"
return 9 # Failed
fi
fi
# Set up so works with GUI from command line
xauthority="${home_dir}.Xauthority"
if [ ! -e "$xauthority" ]
then
ln -s "$HOME/.Xauthority" "$xauthority"
fi
HOME="$home_dir"
# Start the dropbox daemon
if [[ $verbose -gt 0 ]]; then
echo '~/.dropbox-dist/dropboxd & '$home_dir
fi
~/.dropbox-dist/dropboxd &
if [[ $wait -eq 0 ]]; then
sleep 2 # Give each instance time to startup completely before starting another one
else
read -n 1 -s -p 'Press any key to continue.'
echo
fi
}
function start_dropboxes() {
local dropbox
for dropbox in "${DROPBOXES[#]}"
do
start_dropbox $# -p "$dropbox"
done
}
#
# For testing & setup we can choose just one to startup
#
while getopts f:wv opt; do
case $opt in
f) start_dropbox -p "${DROPBOXES[$OPTARG]}" # NOTE: bash array indexes start at 0.
exit ;;
*) ;;
esac
done
OPTIND=1
start_dropboxes $#
Not being able to install multiple instances of same software on a single machine is a typical case of what is called as Software Conflict where two such instances would compete for resources such as memory, peripheral device, register, network port, etc.
However, we could use a container-based virtualization technology called as Docker to run multiple instances of a software on the same machine in loosely isolated environments called as Containers.
The best part of this solution is that it would work on any platform, as containers are meant to portable.
I recently wrote a blog on it, explaining steps to containerise dropbox instances using docker.

Searching for function in bash script to detect installed software packages

I need some code which checks, if a list of packages is installed. This check should work on each linux distribution (Fedora, Arch-Linux, Debian, RedHat,...).
Actually is my script looking like that:
#!/bin/bash
# At first check, if all needed softwares are installed
declare -a NEEDED_SOFTWARE_LIST=(bash rsync wget grep telnet)
if [ -f /etc/debian_version ]; then
for SOFTWARE in ${NEEDED_SOFTWARE_LIST[#]}; do
dpkg -l | grep -i $SOFTWARE | head -1 | if [[ "$(cut -d ' ' -f 1)" != "ii" ]]; then
echo -e "$SOFTWARE is NOT installed completely! Please install it...\n";
exit 1;
fi
done
else
echo "No Debian";
fi
I hope somebody can help me.
when you say:
This check should work on each linux distribution (Fedora, Arch-Linux, Debian, RedHat,...).
You are close to hell....
There is no easy way to do it with confidence in all distros.
with this approach, you will find that:
there is no reliable way to guess the distro, it depends even from version to version...
there is no reliable way to say "this package is not in the system", you will just assume that if its not installed using that distro's official package manager, it wont be there.
sometimes, people use more than just one package manager.
there is no reliable way to know if the package that you are talking about, has that name in that distro (e.g. - php can be "php" in one distro, "php5" in another one, and both are "php5").
this will become a hard to maintain, easy to fail, never reliable, piece of software.
so if you need to create an installer, and have dependencies, please, use a well known package manager, build a good package, and dont re-invent the wheel.
It might sound daunting, but it will pay off on the long run.
Wouldn't it be simpler to just check that the binary is found?
declare -a NEEDED_SOFTWARE_LIST=(bash rsync wget grep telnet)
for SOFTWARE in ${NEEDED_SOFTWARE_LIST[#]} ; do
$SOFTWARE --version |& grep "command not found" && echo "$SOFTWARE is NOT installed completely! Please install it..." && exit 1
done

Resources