14

I am programming a Linux shell script that will print status banners during its execution only if the proper tool, say figlet, is installed (this is: reachable on system path).

Example:

#!/usr/bin/env bash
echo "foo"
figlet "Starting"
echo "moo"
figlet "Working"
echo "foo moo"
figlet "Finished"

I would like for my script to work without errors even when figlet is not installed.

What could be a practical method?

  • 1
  • @sudodus : just ignoring the 'figlet' (and its parameters) command would be OK. Continuing execution, of course. – Sopalajo de Arrierez Jan 30 at 10:50
  • 2
    The title to this question got me in all sorts of metaphysical problems – g_uint Jan 30 at 12:31
  • 2
    Do you want to ignore all errors? Just use figlet ... || true. – Giacomo Alzetta Jan 30 at 13:40
  • If you don’t care about exit codes a shortcut is to use figlet || true, but in your case probably a shell function which Echos plaintext If no Banner can be printed is more likely what you want. – eckes Jan 31 at 1:26
29

My interpretation would use a wrapper function named the same as the tool; in that function, execute the real tool if it exists:

figlet() {
  command -v figlet >/dev/null && command figlet "$@"
}

Then you can have figlet arg1 arg2... unchanged in your script.

@Olorin came up with a simpler method: define a wrapper function only if we need to (if the tool doesn't exist):

if ! command -v figlet > /dev/null; then figlet() { :; }; fi

If you'd like the arguments to figlet to be printed even if figlet isn't installed, adjust Olorin's suggestion as follows:

if ! command -v figlet > /dev/null; then figlet() { printf '%s\n' "$*"; }; fi
  • 1
    Where does command come from? I don't have it in my installations (Red Hat 6.8 Enterprise and Cygwin64). – eewanco Jan 29 at 22:38
  • 1
    @eewanco, see unix.stackexchange.com/a/85250/117549; long story short, it's built in to bash, which is probably your shell. – Jeff Schaller Jan 29 at 23:09
  • 4
    command is a POSIX Bourne shell builtin. It normally executes the command provided, but the -v flag makes it behave more like type, another shell builtin. – wyrm Jan 30 at 1:29
  • @eewanco type -a command will show you. which will only show executables on your $PATH, not built-ins, keywords, functions or aliases. – l0b0 Jan 30 at 1:38
  • @l0b0: on RedHat-family with bash and the default profile(s) which does find an applicable alias, because it aliases which itself to run /usr/bin/which and pipe it a list of the shell's aliases to look in (!) (Of course \which suppresses the alias and uses only the program, which doesn't show aliases.) – dave_thompson_085 Jan 31 at 14:43
14

You can test to see if figlet exists

if type figlet >/dev/null 2>&1
then
    echo Figlet is installed
fi
6

A common way to do this is with test -x aka [ -x. Here is an example taken from /etc/init.d/ntp on a Linux system:

if [ -x /usr/bin/lockfile-create ]; then
    lockfile-create $LOCKFILE
    lockfile-touch $LOCKFILE &
    LOCKTOUCHPID="$!"
fi

This variant relies on knowing the full path of the executable. In /bin/lesspipe I found an example which works around that by combining -x and the which command:

if [ -x "`which bunzip`" ]; then bunzip -c "$1"
else echo "No bunzip available"; fi ;;

That way this will work without knowing in advance where in the PATH the bunzip executable is.

  • 4
    Don't use which. And even if which worked, using test -x on its output is silly: if you get a path from which, it exists. – Gilles Jan 30 at 9:29
  • 1
    @Gilles: Technically speaking, test -x does more than just check if a file exists (that's what test -e is for). It also checks if the file has the execute permissions set. – comfreak Feb 1 at 19:19
  • @comfreak So does which. – Gilles Feb 1 at 20:25
5

At the start of your script, check if figlet exists, and if it does not, define a shell function that does nothing:

type figlet >/dev/null 2>&1 || figlet() { :; }

type checks if figlet exists as a shell built-in, function, alias, or keyword, >/dev/null 2>&1 discards stdin and stdout so you don't get any output, and if it does not exist, figlet() { :; } defines figlet as a function that does nothing.

This way you don't have to edit every line of your script that uses figlet, or check if it exists every time figlet is called.

You can add a diagnostic message, if you like:

type figlet >/dev/null 2>&1 || { echo 'figlet not installed.' ; figlet() { :; } ; }

As a bonus, since you didn't mention which shell you are using, I believe this is POSIX compliant, so it should work on most any shell.

  • I like the simplicity of this answer. You could also replace type figlet >/dev/null 2>&1 with hash figlet 2>/dev/null if you're using bash. (The OP said "if it's in my PATH".) – Joe Feb 1 at 21:44
5

Another alternative -- a pattern I've seen in project auto configure scripts:

if [ -x /usr/bin/figlet ]
then
    FIGLET=/usr/bin/figlet
else
    FIGLET=:
fi

$FIGLET "Hello, world!"

In your specific case you could even do,

if [ -x /usr/bin/figlet ]
then
   SAY=/usr/bin/figlet
elif [ -x /usr/local/bin/figlet ]
then
   SAY=/usr/local/bin/figlet
elif [ -x /usr/bin/banner ]
then
   SAY=/usr/bin/banner
else
   SAY=/usr/bin/echo
fi

$SAY "Hello, world!"

If you don't know the specific path, you can try multiple elif (see above) to try known locations, or just use the PATH to always resolve the command:

if command -v figlet >/dev/null
then
    SAY=figlet
elif command -v banner >/dev/null
then
    SAY=banner
else
    SAY=echo
fi

In general, when writing scripts, I prefer to only call commands in specific locations specified by me. I don't like the uncertainty/risk of what the end user might have put into their PATH, perhaps in their own ~/bin.

If, for example, I was writing a complicated script for others that might remove files based on the output of a particular command I'm calling, I wouldn't want to accidentally pick up something in their ~/bin that might or might not be the command I expected.

  • 3
    What if figlet was in /usr/local/bin or /home/bob/stuff/programs/executable/figlet? – Gilles Jan 30 at 9:30
3
type -p figlet > /dev/null && figlet "foo"

The bash type command finds a command, function, alias, keyword, or builtin (see help type) and prints out the location or definition. It also returns a return code representing the result of the search; true (0) if found. So what we're doing here is trying to find figlet in the path (-p means only look for files, not built-ins or functions, and also suppresses error messages), discarding output (that's what > /dev/null does), and if it returns true (&&), it will execute figlet.

This is simpler if figlet is in a fixed location:

[ -x /usr/bin/figlet ] && /usr/bin/figlet "foo"

Here we're using the test command (a.k.a. [) to see if /usr/bin/figlet is executable (-x) and if so (&&) execute it. This solution I think is more portable than using type which is a bashism I believe.

You could make a function that does this for you:

function x() {
    if type -p "$1" >/dev/null; then
        cmd="$1"
        shift
        "$cmd" "$@"
    fi
}

(The quotes are necessary owing to potential spaces)

Then you'd just do:

x figlet "foo"
0

o/, I would say something like

#!/usr/bin/env bash
# if figlet is installed :
if [ "$(which figlet 2>/dev/null)" ]; then
       # do what you wanted to do
       echo "foo"
       figlet "Starting"
       echo "moo"
       figlet "Working"
       echo "foo moo"
       figlet "Finished"
# if not
else
       # exit program with an error
       echo "please install figlet"
       exit 1
fi
0

You can do a test execution, suppressing any output, and test success/failure code. Choose arguments to figlet to make this test inexpensive. -? or --help or --version are obvious possibilities.

if figlet --help >/dev/null 2>&1 ; then
    # figlet is available
    echo "foo"
    figlet "starting"
    #etc
else
    rc=$?
    echo "figlet is not installed or not working correctly (return code ${rc})"
fi

Added in response to comment below: if you really want to test that figlet exists, not that it's usable, then you would do

figlet --help >/dev/null 2>&1 
rc=$?
if [ $rc -eq 127 ] ; then # 127 is "command not found" on linux bash 4.4.23(1)
    echo "command figlet not found"
else
    figlet "whatever" # use figlet
fi
  • The problem here is that figlet can fail for reasons besides not being installed, so just testing the exit code doesn't suffice. – Chris Jan 31 at 11:15
  • If you really want to test only for its installation, then you want to test if $? equals 127 (on my linux system). 127 is "command not found". But my thinking is that for rational commands, if command --help fails, then the installation is borked sufficiently that it might as well not be there!. – nigel222 Jan 31 at 12:06
  • 1
    126 is "command found but not executable," so you would want to test against that as well. Unfortunately, you can't depend on --help being available. Posix utilities don't have it, for one, and posix guidelines actually recommend against it. at --help fails with at: invalid option -- '-', for instance, and an exit status of 130 on my system. – Chris Jan 31 at 12:47
  • Also, of course, if figlet can exit with status 127 that could be a problem too. – Chris Jan 31 at 15:14
  • Fair point about 126. Most sane utilities have some sort of lightweight harmless command options, such as -? --help --version ... or you could find out what figlet -0 --illegal exits with, and treat that as your success indicator (as long as it's not 127, which I would class as sabotage). – nigel222 Jan 31 at 15:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.