Unset readonly variable in bash - bash

How do I unset a readonly variable in Bash?
$ readonly PI=3.14
$ unset PI
bash: PI: readonly variable
or is it not possible?

Actually, you can unset a readonly variable. but I must warn that this is a hacky method. Adding this answer, only as information, not as a recommendation. Use it at your own risk. Tested on ubuntu 13.04, bash 4.2.45.
This method involves knowing a bit of bash source code & it's inherited from this answer.
$ readonly PI=3.14
$ unset PI
-bash: unset: PI: cannot unset: readonly variable
$ cat << EOF| sudo gdb
attach $$
call unbind_variable("PI")
detach
EOF
$ echo $PI
$
A oneliner answer is to use the batch mode and other commandline flags, as provided in F. Hauri's answer:
$ sudo gdb -ex 'call unbind_variable("PI")' --pid=$$ --batch
sudo may or may not be needed based on your kernel's ptrace_scope settings. Check the comments on vip9937's answer for more details.

I tried the gdb hack above because I want to unset TMOUT (to disable auto-logout), but on the machine that has TMOUT set as read only, I'm not allowed to use sudo. But since I own the bash process, I don't need sudo. However, the syntax didn't quite work with the machine I'm on.
This did work, though (I put it in my .bashrc file):
# Disable the stupid auto-logout
unset TMOUT > /dev/null 2>&1
if [ $? -ne 0 ]; then
gdb <<EOF > /dev/null 2>&1
attach $$
call unbind_variable("TMOUT")
detach
quit
EOF
fi

Shortly: inspired by anishsane's answer
Edit 2021-11-10: Add (int) to cast unbind_variable result.
But with simplier syntax:
$ gdb -ex 'call (int) unbind_variable("PI")' --pid=$$ --batch
With some improvement, as a function:
My destroy function:
Or How to play with variable meta data. Note usage of rare bashisms: local -n VARIABLE=$1 and ${VARIABLE#a}...
destroy () {
declare -p $1 &>/dev/null || return -1 # Return if variable not exist
local -n variable=$1
local reslne result flags=${variable#a}
[ -z "$flags" ] || [ "${flags//*r*}" ] && {
unset $1 # Don't run gdb if variable is not readonly.
return $?
}
while read -r resline; do
[ "$resline" ] && [ -z "${resline%%\$1 = *}" ] &&
result=${resline##*1 = }
done < <(
exec gdb 2>&1 -ex 'call (int) unbind_variable("'$1'")' --pid=$$ --batch
)
return $result
}
You could copy this to a bash source file called destroy.bash, for sample...
Explanation:
1 destroy () {
2 local -n variable=$1
3 declare -p $1 &>/dev/null || return -1 # Return if variable not exist
4 local reslne result flags=${variable#a}
5 [ -z "$flags" ] || [ "${flags//*r*}" ] && {
6 unset $1 # Don't run gdb if variable is not readonly.
7 return $?
8 }
9 while read resline; do
10 [ "$resline" ] && [ -z "${resline%\$1 = *}" ] &&
11 result=${resline##*1 = }
12 done < <(
13 gdb 2>&1 -ex 'call (int) unbind_variable("'$1'")' --pid=$$ --batch
14 )
15 return $result
16 }
line 2 create a local reference to submited variable.
line 3 prevent running on non existant variable
line 4 store parameter's attributes (meta) into $flags.
lines 5 to 8 will run unset instead of gdb if readonly flag not present
lines 9 to 12 while read ... result= ... done get return code of call (int) unbind_variable() in gdb output
line 13 gdb syntax with use of --pid and --ex (see gdb --help).
line 15 return $result of unbind_variable() command.
In use:
$ . destroy.bash
1st with any regular (read-write) variable:
$ declare PI=$(bc -l <<<'4*a(1)')
$ echo $PI
3.14159265358979323844
$ echo ${PI#a} # flags
$ declare -p PI
declare -- PI="3.14159265358979323844"
$ destroy PI
$ echo $?
0
$ declare -p PI
bash: declare: PI: not found
2nd with read only variable:
$ declare -r PI=$(bc -l <<<'4*a(1)')
$ declare -p PI
declare -r PI="3.14159265358979323844"
$ echo ${PI#a} # flags
r
$ unset PI
bash: unset: PI: cannot unset: readonly variable
$ destroy PI
$ echo $?
0
$ declare -p PI
bash: declare: PI: not found
3rd with non existant variable:
$ destroy PI
$ echo $?
255

In zsh,
% typeset +r PI
% unset PI
(Yes, I know the question says bash. But when you Google for zsh, you also get a bunch of bash questions.)

Using GDB is terribly slow, or may even be forbidden by system policy (ie can't attach to process.)
Try ctypes.sh instead. It works by using libffi to directly call bash's unbind_variable() instead, which is every bit as fast as using any other bash builtin:
$ readonly PI=3.14
$ unset PI
bash: unset: PI: cannot unset: readonly variable
$ source ctypes.sh
$ dlcall unbind_variable string:PI
$ declare -p PI
bash: declare: PI: not found
First you will need to install ctypes.sh:
$ git clone https://github.com/taviso/ctypes.sh.git
$ cd ctypes.sh
$ ./autogen.sh
$ ./configure
$ make
$ sudo make install
See https://github.com/taviso/ctypes.sh for a full description and docs.
For the curious, yes this lets you call any function within bash, or any function in any library linked to bash, or even any external dynamically-loaded library if you like. Bash is now every bit as dangerous as perl... ;-)

According to the man page:
unset [-fv] [name ...]
... Read-only variables may not be
unset. ...
If you have not yet exported the variable, you can use exec "$0" "$#" to restart your shell, of course you will lose all other un-exported variables as well. It seems if you start a new shell without exec, it loses its read-only property for that shell.

Specifically wrt to the TMOUT variable. Another option if gdb is not available is to copy bash to your home directory and patch the TMOUT string in the binary to something else, for instance XMOUX. And then run this extra layer of shell and you will not be timed out.

$ PI=3.17
$ export PI
$ readonly PI
$ echo $PI
3.17
$ PI=3.14
-bash: PI: readonly variable
$ echo $PI
3.17
What to do now?
$ exec $BASH
$ echo $PI
3.17
$ PI=3.14
$ echo $PI
3.14
$
A subshell can inherit the parent's variables, but won't inherit their protected status.

readonly command makes it final and permanent until the shell process terminates. If you need to change a variable, don't mark it readonly.

An alternative if gdb is unavailable: You can use the enable command to load a custom builtin that will let you unset the read-only attribute. The gist of the code that does it:
SETVARATTR (find_variable ("TMOUT"), att_readonly, 1);
Obviously, you'd replace TMOUT with the variable you care about.
If you don't want to turn that into a builtin yourself, I forked bash in GitHub and added a fully-written and ready-to-compile loadable builtin called readwrite. The commit is at https://github.com/josephcsible/bash/commit/bcec716f4ca958e9c55a976050947d2327bcc195. If you want to use it, get the Bash source with my commit, run ./configure && make && make loadables to build it, then enable -f examples/loadables/readwrite readwrite to add it to your running session, then readwrite TMOUT to use it.

No, not in the current shell. If you wish to assign a new value to it, you will have to fork a new shell where it will have a new meaning and will not be considered as read only.
$ { ( readonly pi=3.14; echo $pi ); pi=400; echo $pi; unset pi; echo [$pi]; }
3.14
400
[]

You can't, from manual page of unset:
For each name, remove the corresponding variable or function. If no options are supplied, or the -v option is given, each name
refers to a shell variable. Read-only variables may not be unset. If -f is specifed, each name refers to a shell function, and the
function definition is removed. Each unset variable or function is removed from the environment passed to subsequent commands. If
any of RANDOM, SECONDS, LINENO, HISTCMD, FUNCNAME, GROUPS, or DIRSTACK are unset, they lose their special properties, even if they
are subsequently reset. The exit status is true unless a name is readonly.

One other way to "unset" a read-only variable in Bash is to declare that variable read-only in a disposable context:
foo(){ declare -r PI=3.14; baz; }
bar(){ local PI=3.14; baz; }
baz(){ PI=3.1415927; echo PI=$PI; }
foo;
bash: PI: readonly variable
bar;
PI=3.1415927
While this is not "unsetting" within scope, which is probably the intent of the original author, this is definitely setting a variable read-only from the point of view of baz() and then later making it read-write from the point of view of baz(), you just need to write your script with some forethought.

Another solution without GDB or an external binary, (in fact an emphasis on Graham Nicholls comment) would be the use of exec.
In my case there were an annoying read-only variable set in /etc/profile.d/xxx.
Quoting the bash manual:
"When bash is invoked as an interactive login shell [...] it first reads and executes commands from the file /etc/profile" [...]
When an interactive shell that is not a login shell is started, bash reads and executes commands from /etc/bash.bashrc [...]
The gist of my workaround was to put in my ~/.bash_profile:
if [ -n "$annoying_variable" ]
then exec env annoying_variable='' /bin/bash
# or: then exec env -i /bin/bash
fi
Warning: to avoid a recursion (which would lock you out if you can only access your account through SSH), one should ensure the "annoying variable" will not be automatically set by the bashrc or to set another variable on the check, for example:
if [ -n "$annoying_variable" ] && [ "${SHLVL:-1}" = 1 ]
then exec env annoying_variable='' SHLVL=$((SHLVL+1)) ${SHELL:-/bin/bash}
fi

$ readonly PI=3.14
$ unset PI
bash: PI: readonly variable
$ gdb --batch-silent --pid=$$ --eval-command='call (int) unbind_variable("PI")'
$ [[ ! -v PI ]] && echo "PI is unset ✔️"
PI is unset ✔️
Notes:
Tested with bash 5.0.17 and gdb 10.1.
The -v varname test was added in bash 4.2. It is "True if the shell variable varname is set (has been assigned a value)." – bash reference manual
Note the cast to int. Without that, the following error will result: 'unbind_variable' has unknown return type; cast the call to its declared return type. The bash source code shows that the return type of the unbind_variable function is int.
This answer is essentially the same as an answer over at superuser.com. I added the cast to int to get past the unknown return type error.

if nothing helps, you could go back in time, to a time where readonly vars were not yet implemented:
env ENV=$HOME/.profile /bin/sh
and in $HOME/.profile show some good will and say
export TMOUT=901
This gives you one extra second before you are logged out :-)

Related

Forcing string replacement in declared function of shell script

I'm working on a script to move some files to a remote server (see:
Function calls in Here Document for unix shell script for more details). In order to allow the script to work both on a local machine and for a remote server, I'm using 'declare -f' to wrap an existing function to be executed remotely. So far I have come up with this:
myscript.sh
REMOTE_HOST=myhost
TMP=eyerep-files
getMoveCommand()
{
echo Src Dir: $2
sudo cp ~/$TMP/start.ini ~/$1/start_b.ini
ls ~/$2
echo Target Dir: $1
ls ~/$1
}
moveRemote()
{
echo "attempting move with here doc"
echo $(declare -fp getMoveCommand )
ssh -t "$REMOTE_HOST" "$(declare -fp getMoveCommand); getMoveCommand ${1#Q} ${TMP#Q}"
}
moveFiles()
{
case "$1" in
# remote deploy
remote)
moveRemote $2
;;
# local deploy
local)
getMoveCommand $2
;;
*)
echo "Usage: myscript.sh {local|remote}"
exit 1
;;
esac
}
moveFiles $1 $2
exit 0
If called with './myscript.sh remote dev' the script should ssh into the remote server and move a file from one folder to another. The problem I'm running into is the string replacement. I have a bunch of global variables acting as constants that getMoveCommand needs access to. In the example here there is only one (TMP) so I can simply pass it as an argument. In the actual script however, the work being done is more complicated and the number of arguments that would need to be passed in would make this solution unwieldy. Since those variables are never expected to change, it seems like it should be possible to force the string replacement to occur before sending the wrapped function along to ssh.
Is what I want to do possible, and if so how? If not, is there another way to handle this that doesn't require passing a large number of arguments to the function?
It is possible to use envsubst if you export the variable:
export TMP=foo
getMoveCommand() {
echo TMP is $TMP
}
declare -fp getMoveCommand|envsubst
The script above prints:
getMoveCommand ()
{
echo TMP is foo
}
You can also send global variables using declare -p:
ssh -t "$REMOTE_HOST" "$(declare -fp getMoveCommand; declare -p GLOBAL_VAR_1 GLOBAL_VAR_2)"$'\n'"getMoveCommand ${1#Q} ${TMP#Q}"
You can also have another global variable that declares them so you can expand them easily:
GLOBAL_VARS=(GLOBAL_VAR_1 GLOBAL_VAR_2)
...
ssh -t "$REMOTE_HOST" "$(declare -fp getMoveCommand; declare -p "${GLOBAL_VARS[#]}")"$'\n'"getMoveCommand ${1#Q} ${TMP#Q}"
If your variables have a common prefix, you can also expand them through "${!PREFIX#}". No need to store to a variable.
Or might as well create an "export" function to keep things cleaner:
dump_env() {
declare -fp getMoveCommand
declare -p GLOBAL_VAR_1 GLOBAL_VAR_2
}
...
ssh -t "$REMOTE_HOST" "$(dump_env)"$'\n'"getMoveCommand ${1#Q} ${TMP#Q}"

Disable functions/aliases in a sourced script

I know I can run an "original" command (not alias) using either \ or "":
\ls
"ls"
This doesn't work for functions though. Also it requires me to use that syntax every time.
Is it possible in a sourced script to disable all functions/aliases from the parent process (one which runs my script)? I.e. if a user in their terminal has some aliases functions defined I want them disabled in my script (but of course I still want to be able to define and use aliases/functions of my own).
Types of Commands in Bash
Bash knows different types of commands which can shadow each other. The precedence of these types is:
aliases
can be defined by the user using alias cmd=...
functions
can be defined by the user using cmd() { ... }
built-ins
are directly implement in bash and cannot be altered. help and enable list all built-ins.
Executable files in $PATH
Meaning if you type cmd arg1 arg2 ... you use the alias cmd if it is defined, otherwise you use the function cmd if it is defined, otherwise you use the built-in cmd if it is built-in, otherwise you use the first executable cmd from the directories in $PATH if there is one, otherwise you end up with the error -bash: cmd command not found.
Which of these cases applies for cmd can be checked using type -a cmd.
Manual Precedence Control
Bash allows you to influence which type to pick using quoting and the built-ins command and builtin.
\cmd
suppresses aliases
uses functions, built-ins, executables
command cmd
suppresses aliases and functions
uses built-ins and executables
builtin cmd
supresses aliases, functions, and executables
uses only built-ins
enable -n cmd
disables the built-in cmd completely, such that afterwards only
aliases, functions, and executables are used
env cmd
not a bash built-in, therefore it doesn't really suppress anything but
uses only executables
Examples
Shadowing is perfectly normal. For instance, bash has its own built-in echo, but your system also has /bin/echo. Both implementations may differ. For instance, my echo from bash 5 supports \uXXXX but my echo from GNU coreutils 8.3 does not. The possibility of such differences becomes even more clear if you add your own implementations using aliases and functions. Here's an example in an interactive bash session ($ is the prompt):
$ echo() { printf "function echo: %s\n" "$*"; }
$ alias echo='printf "alias echo: %s %s %s\n"'
$ type -a echo
echo is aliased to `printf "alias echo: %s %s %s\n"'
echo is a function
echo ()
{
printf "function echo: %s\n" "$*"
}
echo is a shell builtin
echo is /bin/echo
$ echo -e '\u2261'
alias echo: -e \u2261
$ \echo -e '\u2261'
function echo: -e \u2261
# use the built-in (or executable file if there was no such built-in)
$ command echo -e '\u2261'
≡
$ builtin echo -e '\u2261'
≡
# use the executable /bin/echo
$ env echo -e '\u2261'
\u2261
$ enable -n echo
# use the executable /bin/echo (`command` is needed to skip the alias and function)
$ command echo -e '\u2261'
\u2261
Answering your Question
Unfortunately I'm not aware of something like enable to permanently disable alias and function lookup. You could try some hacks like backing up all aliases and functions, doing unset -f and unalias on them, and restoring them at the end. However, unset may fail for readonly functions. The better way would be to use bash -c '... functions and aliases have no effect here ...' for the parts where you don't really need the benefits of source. For the other parts, prefix everything with command.
Please note: The caller who sources your script may even disable or shadow command, builtin, and so on -- therefore you can never be sure that you are actually using the commands you expected. Even writing /usr/bin/env executable or /path/to/the/executable does not help as a function can have the name and $PATH or the file system can be altered.
However, that shouldn't be your concern. The one who sources your script should be responsible for providing the correct environment.
Edit: this answer might no longer be relevant since you edited the question to clarify that the script is being sourced, not being executed in a subshell.
This happens by default. Proof:
$ function x() { echo 'hi'; }
$ x
hi
$ bash
# We are now in a subshell.
$ x
bash: x: command not found
Functions are often defined in one of the shell's startup files: .bashrc, .profile or .bash_profile. Which of these are sourced depends on whether the shell is a login shell and/or an interactive shell. A shell that invoked to execute a shell script is neither a login shell nor an interactive shell, and in this case none of those files are sourced.
EDIT: I should read more carefully, as you don't want to source a script, but be sourced, the following is for the other way around:
Functions
If you source your parent script at the beginning, you can just loop through the defined functions and unset them.
declare -F will list all defined functions but in the format declare -f functioname, so you have to get only the name:
IFS=$'\n'
for f in $(declare -F|cut -d ' ' -f 3); do
unset -f $f
done
Aliases
Alias should not be sourced in as i remember, but if they are there you can do
unalias -a
to unset them all.

BASH build dynamic command [duplicate]

I have a shell script file like this:
#!/bin/bash
CONF_FILE="/tmp/settings.conf" #settings.conf contains OS_NAME="Caine Linux"
source $CONF_FILE
display_os_name() { echo "My OS is:" $OS_NAME }
#using the function locally works fine
display_os_name
#displays: My OS is: Caine Linux
#using the function on the remote host doesn't work
ssh user#host "$(declare -f); display_os_name"
#displays: My OS is:
If I remove the -f and I use just ssh user#host "$(declare); display_os_name" it works but displays these errors and warnings:
bash: line 10: BASHOPTS: readonly variable
bash: line 18: BASH_VERSINFO: readonly variable
bash: line 26: EUID: readonly variable
bash: line 55: PPID: readonly variable
bash: line 70: SHELLOPTS: readonly variable
bash: line 76: UID: readonly variable
If I use ssh user#host "$(declare); display_os_name >/dev/null" to suppress the warnings only the output of the function is suppressed (My OS is: Caine Linux), not the warnings.
Is there a way to run local functions together with sourced local files on a remote SSH host?
An easy approach (if your local side is Linux) is to use set -a to enable automatic export before your source command; copy /proc/self/environ on stdin; and parse it into a set of variables on the remote side.
Because BASHOPTS, EUID, etc. aren't environment variables, this avoids trying to modify them. (If you were complying with POSIX recommendations and using lowercase names for your own variables, you could even go as far as to ignore all-caps variables entirely).
set -a # enable export of all variables defined, **before** the source operation
source /tmp/settings.conf
import_env() {
while IFS= read -r -d '' item; do
printf -v "${item%%=*}" "%s" "${item#*=}" && export "$item"
done
}
cat /proc/self/environ | ssh user#host "$(declare -f); import_env; display_os_name"
Even easier is to just copy the file you want to source over the wire.
ssh user#host "$(declare -f); $(</tmp/settings.conf); display_os_name"
This method works using GNU bash, version 5.1.4(1)-release (x86_64-pc-linux-gnu)
#!/bin/bash
#################################################################################
source $CONF_FILE
#settings.conf contains OS_NAME="Caine Linux"
CONF_FILE="/tmp/settings.conf"
special_file='!abc123'
OS_NAME='my_server'
display_os_name()
{
echo "My OS is:" $OS_NAME
}
ssh -tt -q user#host << EOT
CONF_FILE=$CONF_FILE
special_file=$\\special_file
OS_NAME=$OS_NAME
$(typset -f display_os_name)
display_os_name
EOT
#################################################################################

Detect if executable file is on user's PATH [duplicate]

This question already has answers here:
How can I check if a program exists from a Bash script?
(39 answers)
Closed 4 years ago.
In a bash script, I need to determine whether an executable named foo is on the PATH.
You could also use the Bash builtin type -P:
help type
cmd=ls
[[ $(type -P "$cmd") ]] && echo "$cmd is in PATH" ||
{ echo "$cmd is NOT in PATH" 1>&2; exit 1; }
You can use which:
path_to_executable=$(which name_of_executable)
if [ -x "$path_to_executable" ] ; then
echo "It's here: $path_to_executable"
fi
TL;DR:
In bash:
function is_bin_in_path {
builtin type -P "$1" &> /dev/null
}
Example usage of is_bin_in_path:
% is_bin_in_path ls && echo "found in path" || echo "not in path"
found in path
In zsh:
Use whence -p instead.
For a version that works in both {ba,z}sh:
# True if $1 is an executable in $PATH
# Works in both {ba,z}sh
function is_bin_in_path {
if [[ -n $ZSH_VERSION ]]; then
builtin whence -p "$1" &> /dev/null
else # bash:
builtin type -P "$1" &> /dev/null
fi
}
To test that ALL given commands are executables in $PATH:
# True iff all arguments are executable in $PATH
function is_bin_in_path {
if [[ -n $ZSH_VERSION ]]; then
builtin whence -p "$1" &> /dev/null
else # bash:
builtin type -P "$1" &> /dev/null
fi
[[ $? -ne 0 ]] && return 1
if [[ $# -gt 1 ]]; then
shift # We've just checked the first one
is_bin_in_path "$#"
fi
}
Example usage:
is_bin_in_path ssh-agent ssh-add && setup_ssh_agent
Non-solutions to avoid
This is not a short answer because the solution must correctly handle:
Functions
Aliases
Builtin commands
Reserved words
Examples which fail with plain type (note the token after type changes):
$ alias foo=ls
$ type foo && echo "in path" || echo "not in path"
foo is aliased to `ls'
in path
$ type type && echo "in path" || echo "not in path"
type is a shell builtin
in path
$ type if && echo "in path" || echo "not in path"
if is a shell keyword
in path
Note that in bash, which is not a shell builtin (it is in zsh):
$ PATH=/bin
$ builtin type which
which is /bin/which
This answer says why to avoid using which:
Avoid which. Not only is it an external process you're launching for doing very little (meaning builtins like hash, type or command are way cheaper), you can also rely on the builtins to actually do what you want, while the effects of external commands can easily vary from system to system.
Why care?
Many operating systems have a which that doesn't even set an exit status, meaning the if which foo won't even work there and will always report that foo exists, even if it doesn't (note that some POSIX shells appear to do this for hash too).
Many operating systems make which do custom and evil stuff like change the output or even hook into the package manager.
In this case, also avoid command -v
The answer I just quoted from suggests using command -v, however this doesn't apply to the current "is the executable in $PATH?" scenario: it will fail in exactly the ways I've illustrated with plain type above.
Correct solutions
In bash we need to use type -P:
-P force a PATH search for each NAME, even if it is an alias,
builtin, or function, and returns the name of the disk file
that would be executed
In zsh we need to use whence -p:
-p Do a path search for name even if it is an alias,
reserved word, shell function or builtin.
You can use the command builtin, which is POSIX compatible:
if [ -x "$(command -v "$cmd")" ]; then
echo "$cmd is in \$PATH"
fi
The executable check is needed because command -v detects functions and aliases as well as executables.
In Bash, you can also use type with the -P option, which forces a PATH search:
if type -P "$cmd" &>/dev/null; then
echo "$cmd is in \$PATH"
fi
As already mentioned in the comments, avoid which as it requires launching an external process and might give you incorrect output in some cases.
if command -v foo ; then foo ; else echo "foo unavailable" ; fi
Use which
$ which myprogram
We can define a function for checking whether as executable exists by using which:
function is_executable() {
which "$#" &> /dev/null
}
The function is called just like you would call an executable. "$#" ensures that which gets exactly the same arguments as are given to the function.
&> /dev/null ensures that whatever is written to stdout or stderr by which is redirected to the null device (which is a special device which discards the information written to it) and not written to stdout or stderr by the function.
Since the function doesn't explicitly return with an return code, when it does return, the exit code of the latest executed executable—which in this case is which—will be the return code of the function. which will exit with a code that indicates success if it is able to find the executable specified by the argument to the function, otherwise with an exit code that indicates failure. This behavior will automatically be replicated by is_executable.
We can then use that function to conditionally do something:
if is_executable name_of_executable; then
echo "name_of_executable was found"
else
echo "name_of_executable was NOT found"
fi
Here, if executes the command(s) written between it and then—which in our case is is_executable name_of_executable—and chooses the branch to execute based on the return code of the command(s).
Alternatively, we can skip defining the function and use which directly in the if-statement:
if which name_of_executable &> /dev/null; then
echo "name_of_executable was found"
else
echo "name_of_executable was NOT found"
fi
However, I think this makes the code slightly less readable.

How can I check if a program exists from a Bash script?

How would I validate that a program exists, in a way that will either return an error and exit, or continue with the script?
It seems like it should be easy, but it's been stumping me.
Answer
POSIX compatible:
command -v <the_command>
Example use:
if ! command -v <the_command> &> /dev/null
then
echo "<the_command> could not be found"
exit
fi
For Bash specific environments:
hash <the_command> # For regular commands. Or...
type <the_command> # To check built-ins and keywords
Explanation
Avoid which. Not only is it an external process you're launching for doing very little (meaning builtins like hash, type or command are way cheaper), you can also rely on the builtins to actually do what you want, while the effects of external commands can easily vary from system to system.
Why care?
Many operating systems have a which that doesn't even set an exit status, meaning the if which foo won't even work there and will always report that foo exists, even if it doesn't (note that some POSIX shells appear to do this for hash too).
Many operating systems make which do custom and evil stuff like change the output or even hook into the package manager.
So, don't use which. Instead use one of these:
command -v foo >/dev/null 2>&1 || { echo >&2 "I require foo but it's not installed. Aborting."; exit 1; }
type foo >/dev/null 2>&1 || { echo >&2 "I require foo but it's not installed. Aborting."; exit 1; }
hash foo 2>/dev/null || { echo >&2 "I require foo but it's not installed. Aborting."; exit 1; }
(Minor side-note: some will suggest 2>&- is the same 2>/dev/null but shorter – this is untrue. 2>&- closes FD 2 which causes an error in the program when it tries to write to stderr, which is very different from successfully writing to it and discarding the output (and dangerous!))
If your hash bang is /bin/sh then you should care about what POSIX says. type and hash's exit codes aren't terribly well defined by POSIX, and hash is seen to exit successfully when the command doesn't exist (haven't seen this with type yet). command's exit status is well defined by POSIX, so that one is probably the safest to use.
If your script uses bash though, POSIX rules don't really matter anymore and both type and hash become perfectly safe to use. type now has a -P to search just the PATH and hash has the side-effect that the command's location will be hashed (for faster lookup next time you use it), which is usually a good thing since you probably check for its existence in order to actually use it.
As a simple example, here's a function that runs gdate if it exists, otherwise date:
gnudate() {
if hash gdate 2>/dev/null; then
gdate "$#"
else
date "$#"
fi
}
Alternative with a complete feature set
You can use scripts-common to reach your need.
To check if something is installed, you can do:
checkBin <the_command> || errorMessage "This tool requires <the_command>. Install it please, and then run this tool again."
The following is a portable way to check whether a command exists in $PATH and is executable:
[ -x "$(command -v foo)" ]
Example:
if ! [ -x "$(command -v git)" ]; then
echo 'Error: git is not installed.' >&2
exit 1
fi
The executable check is needed because bash returns a non-executable file if no executable file with that name is found in $PATH.
Also note that if a non-executable file with the same name as the executable exists earlier in $PATH, dash returns the former, even though the latter would be executed. This is a bug and is in violation of the POSIX standard. [Bug report] [Standard]
Edit: This seems to be fixed as of dash 0.5.11 (Debian 11).
In addition, this will fail if the command you are looking for has been defined as an alias.
I agree with lhunath to discourage use of which, and his solution is perfectly valid for Bash users. However, to be more portable, command -v shall be used instead:
$ command -v foo >/dev/null 2>&1 || { echo "I require foo but it's not installed. Aborting." >&2; exit 1; }
Command command is POSIX compliant. See here for its specification: command - execute a simple command
Note: type is POSIX compliant, but type -P is not.
It depends on whether you want to know whether it exists in one of the directories in the $PATH variable or whether you know the absolute location of it. If you want to know if it is in the $PATH variable, use
if which programname >/dev/null; then
echo exists
else
echo does not exist
fi
otherwise use
if [ -x /path/to/programname ]; then
echo exists
else
echo does not exist
fi
The redirection to /dev/null/ in the first example suppresses the output of the which program.
I have a function defined in my .bashrc that makes this easier.
command_exists () {
type "$1" &> /dev/null ;
}
Here's an example of how it's used (from my .bash_profile.)
if command_exists mvim ; then
export VISUAL="mvim --nofork"
fi
Expanding on #lhunath's and #GregV's answers, here's the code for the people who want to easily put that check inside an if statement:
exists()
{
command -v "$1" >/dev/null 2>&1
}
Here's how to use it:
if exists bash; then
echo 'Bash exists!'
else
echo 'Your system does not have Bash'
fi
Try using:
test -x filename
or
[ -x filename ]
From the Bash manpage under Conditional Expressions:
-x file
True if file exists and is executable.
To use hash, as #lhunath suggests, in a Bash script:
hash foo &> /dev/null
if [ $? -eq 1 ]; then
echo >&2 "foo not found."
fi
This script runs hash and then checks if the exit code of the most recent command, the value stored in $?, is equal to 1. If hash doesn't find foo, the exit code will be 1. If foo is present, the exit code will be 0.
&> /dev/null redirects standard error and standard output from hash so that it doesn't appear onscreen and echo >&2 writes the message to standard error.
Command -v works fine if the POSIX_BUILTINS option is set for the <command> to test for, but it can fail if not. (It has worked for me for years, but I recently ran into one where it didn't work.)
I find the following to be more failproof:
test -x "$(which <command>)"
Since it tests for three things: path, existence and execution permission.
There are a ton of options here, but I was surprised no quick one-liners. This is what I used at the beginning of my scripts:
[[ "$(command -v mvn)" ]] || { echo "mvn is not installed" 1>&2 ; exit 1; }
[[ "$(command -v java)" ]] || { echo "java is not installed" 1>&2 ; exit 1; }
This is based on the selected answer here and another source.
If you check for program existence, you are probably going to run it later anyway. Why not try to run it in the first place?
if foo --version >/dev/null 2>&1; then
echo Found
else
echo Not found
fi
It's a more trustworthy check that the program runs than merely looking at PATH directories and file permissions.
Plus you can get some useful result from your program, such as its version.
Of course the drawbacks are that some programs can be heavy to start and some don't have a --version option to immediately (and successfully) exit.
Check for multiple dependencies and inform status to end users
for cmd in latex pandoc; do
printf '%-10s' "$cmd"
if hash "$cmd" 2>/dev/null; then
echo OK
else
echo missing
fi
done
Sample output:
latex OK
pandoc missing
Adjust the 10 to the maximum command length. It is not automatic, because I don't see a non-verbose POSIX way to do it:
How can I align the columns of a space separated table in Bash?
Check if some apt packages are installed with dpkg -s and install them otherwise.
See: Check if an apt-get package is installed and then install it if it's not on Linux
It was previously mentioned at: How can I check if a program exists from a Bash script?
I never did get the previous answers to work on the box I have access to. For one, type has been installed (doing what more does). So the builtin directive is needed. This command works for me:
if [ `builtin type -p vim` ]; then echo "TRUE"; else echo "FALSE"; fi
I wanted the same question answered but to run within a Makefile.
install:
#if [[ ! -x "$(shell command -v ghead)" ]]; then \
echo 'ghead does not exist. Please install it.'; \
exit -1; \
fi
It could be simpler, just:
#!/usr/bin/env bash
set -x
# if local program 'foo' returns 1 (doesn't exist) then...
if ! type -P foo; then
echo 'crap, no foo'
else
echo 'sweet, we have foo!'
fi
Change foo to vi to get the other condition to fire.
hash foo 2>/dev/null: works with Z shell (Zsh), Bash, Dash and ash.
type -p foo: it appears to work with Z shell, Bash and ash (BusyBox), but not Dash (it interprets -p as an argument).
command -v foo: works with Z shell, Bash, Dash, but not ash (BusyBox) (-ash: command: not found).
Also note that builtin is not available with ash and Dash.
zsh only, but very useful for zsh scripting (e.g. when writing completion scripts):
The zsh/parameter module gives access to, among other things, the internal commands hash table. From man zshmodules:
THE ZSH/PARAMETER MODULE
The zsh/parameter module gives access to some of the internal hash ta‐
bles used by the shell by defining some special parameters.
[...]
commands
This array gives access to the command hash table. The keys are
the names of external commands, the values are the pathnames of
the files that would be executed when the command would be in‐
voked. Setting a key in this array defines a new entry in this
table in the same way as with the hash builtin. Unsetting a key
as in `unset "commands[foo]"' removes the entry for the given
key from the command hash table.
Although it is a loadable module, it seems to be loaded by default, as long as zsh is not used with --emulate.
example:
martin#martin ~ % echo $commands[zsh]
/usr/bin/zsh
To quickly check whether a certain command is available, just check if the key exists in the hash:
if (( ${+commands[zsh]} ))
then
echo "zsh is available"
fi
Note though that the hash will contain any files in $PATH folders, regardless of whether they are executable or not. To be absolutely sure, you have to spend a stat call on that:
if (( ${+commands[zsh]} )) && [[ -x $commands[zsh] ]]
then
echo "zsh is available"
fi
The which command might be useful. man which
It returns 0 if the executable is found and returns 1 if it's not found or not executable:
NAME
which - locate a command
SYNOPSIS
which [-a] filename ...
DESCRIPTION
which returns the pathnames of the files which would
be executed in the current environment, had its
arguments been given as commands in a strictly
POSIX-conformant shell. It does this by searching
the PATH for executable files matching the names
of the arguments.
OPTIONS
-a print all matching pathnames of each argument
EXIT STATUS
0 if all specified commands are
found and executable
1 if one or more specified commands is nonexistent
or not executable
2 if an invalid option is specified
The nice thing about which is that it figures out if the executable is available in the environment that which is run in - it saves a few problems...
Use Bash builtins if you can:
which programname
...
type -P programname
For those interested, none of the methodologies in previous answers work if you wish to detect an installed library. I imagine you are left either with physically checking the path (potentially for header files and such), or something like this (if you are on a Debian-based distribution):
dpkg --status libdb-dev | grep -q not-installed
if [ $? -eq 0 ]; then
apt-get install libdb-dev
fi
As you can see from the above, a "0" answer from the query means the package is not installed. This is a function of "grep" - a "0" means a match was found, a "1" means no match was found.
This will tell according to the location if the program exist or not:
if [ -x /usr/bin/yum ]; then
echo "This is Centos"
fi
I'd say there isn't any portable and 100% reliable way due to dangling aliases. For example:
alias john='ls --color'
alias paul='george -F'
alias george='ls -h'
alias ringo=/
Of course, only the last one is problematic (no offence to Ringo!). But all of them are valid aliases from the point of view of command -v.
In order to reject dangling ones like ringo, we have to parse the output of the shell built-in alias command and recurse into them (command -v isn't a superior to alias here.) There isn't any portable solution for it, and even a Bash-specific solution is rather tedious.
Note that a solution like this will unconditionally reject alias ls='ls -F':
test() { command -v $1 | grep -qv alias }
If you guys/gals can't get the things in answers here to work and are pulling hair out of your back, try to run the same command using bash -c. Just look at this somnambular delirium. This is what really happening when you run $(sub-command):
First. It can give you completely different output.
$ command -v ls
alias ls='ls --color=auto'
$ bash -c "command -v ls"
/bin/ls
Second. It can give you no output at all.
$ command -v nvm
nvm
$ bash -c "command -v nvm"
$ bash -c "nvm --help"
bash: nvm: command not found
#!/bin/bash
a=${apt-cache show program}
if [[ $a == 0 ]]
then
echo "the program doesn't exist"
else
echo "the program exists"
fi
#program is not literal, you can change it to the program's name you want to check
The hash-variant has one pitfall: On the command line you can for example type in
one_folder/process
to have process executed. For this the parent folder of one_folder must be in $PATH. But when you try to hash this command, it will always succeed:
hash one_folder/process; echo $? # will always output '0'
I second the use of "command -v". E.g. like this:
md=$(command -v mkdirhier) ; alias md=${md:=mkdir} # bash
emacs="$(command -v emacs) -nw" || emacs=nano
alias e=$emacs
[[ -z $(command -v jed) ]] && alias jed=$emacs
I had to check if Git was installed as part of deploying our CI server. My final Bash script was as follows (Ubuntu server):
if ! builtin type -p git &>/dev/null; then
sudo apt-get -y install git-core
fi
To mimic Bash's type -P cmd, we can use the POSIX compliant env -i type cmd 1>/dev/null 2>&1.
man env
# "The option '-i' causes env to completely ignore the environment it inherits."
# In other words, there are no aliases or functions to be looked up by the type command.
ls() { echo 'Hello, world!'; }
ls
type ls
env -i type ls
cmd=ls
cmd=lsx
env -i type $cmd 1>/dev/null 2>&1 || { echo "$cmd not found"; exit 1; }
If there isn't any external type command available (as taken for granted here), we can use POSIX compliant env -i sh -c 'type cmd 1>/dev/null 2>&1':
# Portable version of Bash's type -P cmd (without output on stdout)
typep() {
command -p env -i PATH="$PATH" sh -c '
export LC_ALL=C LANG=C
cmd="$1"
cmd="`type "$cmd" 2>/dev/null || { echo "error: command $cmd not found; exiting ..." 1>&2; exit 1; }`"
[ $? != 0 ] && exit 1
case "$cmd" in
*\ /*) exit 0;;
*) printf "%s\n" "error: $cmd" 1>&2; exit 1;;
esac
' _ "$1" || exit 1
}
# Get your standard $PATH value
#PATH="$(command -p getconf PATH)"
typep ls
typep builtin
typep ls-temp
At least on Mac OS X v10.6.8 (Snow Leopard) using Bash 4.2.24(2) command -v ls does not match a moved /bin/ls-temp.
My setup for a Debian server:
I had the problem when multiple packages contained the same name.
For example apache2. So this was my solution:
function _apt_install() {
apt-get install -y $1 > /dev/null
}
function _apt_install_norecommends() {
apt-get install -y --no-install-recommends $1 > /dev/null
}
function _apt_available() {
if [ `apt-cache search $1 | grep -o "$1" | uniq | wc -l` = "1" ]; then
echo "Package is available : $1"
PACKAGE_INSTALL="1"
else
echo "Package $1 is NOT available for install"
echo "We can not continue without this package..."
echo "Exitting now.."
exit 0
fi
}
function _package_install {
_apt_available $1
if [ "${PACKAGE_INSTALL}" = "1" ]; then
if [ "$(dpkg-query -l $1 | tail -n1 | cut -c1-2)" = "ii" ]; then
echo "package is already_installed: $1"
else
echo "installing package : $1, please wait.."
_apt_install $1
sleep 0.5
fi
fi
}
function _package_install_no_recommends {
_apt_available $1
if [ "${PACKAGE_INSTALL}" = "1" ]; then
if [ "$(dpkg-query -l $1 | tail -n1 | cut -c1-2)" = "ii" ]; then
echo "package is already_installed: $1"
else
echo "installing package : $1, please wait.."
_apt_install_norecommends $1
sleep 0.5
fi
fi
}

Resources