Linux Professional Institute Learning Logo.
Skip to main content
  • Home
    • All Resources
    • LPI Learning Materials
    • Become a Contributor
    • Publishing Partners
    • Become a Publishing Partner
    • About
    • FAQ
    • Contributors
    • Contact
  • LPI.org
105.1 Lesson 3
Topic 105: Shells and Shell Scripting
105.1 Customize and use the shell environment
  • 105.1 Lesson 1
  • 105.1 Lesson 2
  • 105.1 Lesson 3
105.2 Customize or write simple scripts
  • 105.2 Lesson 1
  • 105.2 Lesson 2
Topic 106: User Interfaces and Desktops
106.1 Install and configure X11
  • 106.1 Lesson 1
106.2 Graphical Desktops
  • 106.2 Lesson 1
106.3 Accessibility
  • 106.3 Lesson 1
Topic 107: Administrative Tasks
107.1 Manage user and group accounts and related system files
  • 107.1 Lesson 1
  • 107.1 Lesson 2
107.2 Automate system administration tasks by scheduling jobs
  • 107.2 Lesson 1
  • 107.2 Lesson 2
107.3 Localisation and internationalisation
  • 107.3 Lesson 1
Topic 108: Essential System Services
108.1 Maintain system time
  • 108.1 Lesson 1
  • 108.1 Lesson 2
108.2 System logging
  • 108.2 Lesson 1
  • 108.2 Lesson 2
108.3 Mail Transfer Agent (MTA) basics
  • 108.3 Lesson 1
108.4 Manage printers and printing
  • 108.4 Lesson 1
Topic 109: Networking Fundamentals
109.1 Fundamentals of internet protocols
  • 109.1 Lesson 1
  • 109.1 Lesson 2
109.2 Persistent network configuration
  • 109.2 Lesson 1
  • 109.2 Lesson 2
109.3 Basic network troubleshooting
  • 109.3 Lesson 1
  • 109.3 Lesson 2
109.4 Configure client side DNS
  • 109.4 Lesson 1
Topic 110: Security
110.1 Perform security administration tasks
  • 110.1 Lesson 1
110.2 Setup host security
  • 110.2 Lesson 1
110.3 Securing data with encryption
  • 110.3 Lesson 1
  • 110.3 Lesson 2
  1. Topic 105: Shells and Shell Scripting
  2. 105.1 Customize and use the shell environment
  3. 105.1 Lesson 3

105.1 Lesson 3

Certificate:

LPIC-1

Version:

5.0

Topic:

105 Shells and Shell Scripting

Objective:

105.1 Customize and use the shell environment

Lesson:

3 of 3

Introduction

After going through shells, startup scripts and variables in the previous lessons, we will round the whole topic of customizing the shell off by having a look at two very interesting shell elements: aliases and functions. In fact the entire group of variables, aliases and functions — and their influence on one another — is what makes up the shell environment.

The main strength of these two flexible and time-saving shell facilities has to do with the concept of encapsulation: they offer the possibility of putting together — under a single command — a series of repetitive or recurrent commands.

Creating Aliases

An alias is a substitute name for another command(s). It can run like a regular command, but instead executes another command according to the alias definition.

The syntax for declaring aliases is quite straightforward. Aliases are declared by writing the keyword alias followed by the alias assignment. In turn, the alias assignment consists of the alias name, an equal sign and one or more commands:

alias alias_name=command(s)

For example:

$ alias oldshell=sh

This awkward alias will start an instance of the original sh shell when the user types oldshell into the terminal:

$ oldshell
$

The power of aliases lies in that they allow us to write short versions of long commands:

$ alias ls='ls --color=auto'
Note

For information about ls and its colors, type man dir_colors into the terminal.

Likewise, we can create aliases for a series of concatenated commands — the semicolon (;) is used as a delimiter. We can, for instance, have an alias that gives us information about the location of the git executable and its version:

$ alias git_info='which git;git --version'

To invoke an alias, we type its name into the terminal:

$ git_info
/usr/bin/git
git version 2.7.4

The alias command will produce a listing of all available aliases in the system:

$ alias
alias git-info='which git;git --version'
alias ls='ls --color=auto'
alias oldshell='sh'

The unalias command removes aliases. We can, for instance, unalias git-info and see how it disappears from the listing:

$ unalias git-info
$ alias
alias ls='ls --color=auto'
alias oldshell='sh'

As we saw with the alias hi='echo We salute you.' in a previous lesson, we must enclose commands in quotes (either single or double) when — as a result of having arguments or parameters — they contain spaces:

$ alias greet='echo Hello world!'
$ greet
Hello world!

Commands with spaces include also those with options:

$ alias ll='ls -al'

Now ll will list all files — including the hidden ones (a) — in the long format (l).

We can reference variables in aliases:

$ reptile=uromastyx
$ alias greet='echo Hello $reptile!'
$ greet
Hello uromastyx!

The variable can also be assigned within the alias:

$ alias greet='reptile=tortoise; echo Hello $reptile!'
$ greet
Hello tortoise!

We can escape an alias with \:

$ alias where?='echo $PWD'
$ where?
/home/user2
$ \where?
-bash: where?: command not found

Escaping an alias is useful when an alias has the same name as a regular command. In this case, the alias takes precedence over the original command, which, however, is still be accessible by escaping the alias.

Likewise, we can put an alias inside another alias:

$ where?
/home/user2
$ alias my_home=where?
$ my_home
/home/user2

On top of that, we can also put a function inside an alias as you will be shown below.

Expansion and Evaluation of Quotes in Aliases

When using quotes with environment variables, single quotes make the expansion dynamic:

$ alias where?='echo $PWD'
$ where?
/home/user2
$ cd Music
$ where?
/home/user2/Music

However, with double quotes the expansion is done statically:

$ alias where?="echo $PWD"
$ where?
/home/user2
$ cd Music
$ where?
/home/user2

Persistence of Aliases: Startup Scripts

Just as with variables, for our aliases to gain persistence, we must put them into initialization scripts that are sourced at startup. As we already know, a good file for users to put their personal aliases in is ~/.bashrc. You will probably find some aliases there already (most of them commented out and ready to be used by removing the leading #):

$ grep alias .bashrc
# enable color support of ls and also add handy aliases
    alias ls='ls --color=auto'
    #alias dir='dir --color=
    #alias vdir='vdir --color=
    #alias grep='grep --color=
    #alias fgrep='fgrep --color'
    #alias egrep='egrep --color=
# some more ls aliases
#ll='ls -al'
#alias la='ls -A'
#alias l='ls -CF'
# ~/.bash_aliases, instead of adding them here directly.
if [ -f ~/.bash_aliases ]; then
   . ~/.bash_aliases

As you can read in the last three lines, we are offered the possibility of having our own alias-dedicated file — ~/.bash_aliases — and have it sourced by .bashrc with every system start. So we can go for that option and create and populate such file:

###########
# .bash_aliases:
# a file to be populated by the user's personal aliases (and sourced by ~/.bashrc).
###########
alias git_info='which git;git --version'
alias greet='echo Hello world!'
alias ll='ls -al'
alias where?='echo $PWD'

Creating Functions

Compared to aliases, functions are more programmatic and flexible, specially when it comes to exploiting the full potential of Bash special built-in variables and positional parameters. They are also great to work with flow control structures such as loops or conditionals. We can think of a function as a command which includes logic through blocks or collections of other commands.

Two Syntaxes for Creating Functions

There are two valid syntaxes to define functions.

Using the keyword function

On the one hand, we can use the keyword function, followed by the name of the function and the commands between curly brackets:

function function_name {
command #1
command #2
command #3
.
.
.
command #n
}
Using ()

On the other, we can leave out the keyword function and use two brackets right after the name of the function instead:

function_name() {
command #1
command #2
command #3
.
.
.
command #n
}

It is commonplace to put functions in files or scripts. However, they can also be written directly into the shell prompt with each command on a different line — note PS2(>) indicating a new line after a line break:

$ greet() {
> greeting="Hello world!"
> echo $greeting
> }

Whatever the case — and irrespective of the syntax we choose — , if we decide to skip line breaks and write a function in just one line, commands must be separated by semicolons (note the semicolon after the last command too):

$ greet() { greeting="Hello world!"; echo $greeting; }

bash did not complain when we pressed Enter, so our function is ready to be invoked. To invoke a function, we must type its name into the terminal:

$ greet
Hello world!

Just as with variables and aliases, if we want functions to be persistent across system reboots we have to put them into shell initialization scripts such as /etc/bash.bashrc (global) or ~/.bashrc (local).

Warning

After adding aliases or functions to any startup script file, you should source such files with either . or source for the changes to take effect if you do not want to logout and back in again or reboot the system.

Bash Special Built-in Variables

The Bourne Again Shell comes with a set of special variables which are particularly useful for functions and scripts. These are special because they can only be referenced — not assigned. Here is a list of the most relevant ones:

$?

This variable’s reference expands to the result of the last command run. A value of 0 means success:

$ ps aux | grep bash
user2      420  0.0  0.4  21156  5012 pts/0    Ss   17:10   0:00 -bash
user2      640  0.0  0.0  12784   936 pts/0    S+   18:04   0:00 grep bash
$ echo $?
0

A value other than 0 means error:

user1@debian:~$ ps aux |rep bash
-bash: rep: command not found
user1@debian:~$ echo $?
127
$$

It expands to the shell PID (process ID):

$ ps aux | grep bash
user2      420  0.0  0.4  21156  5012 pts/0    Ss   17:10   0:00 -bash
user2      640  0.0  0.0  12784   936 pts/0    S+   18:04   0:00 grep bash
$ echo $$
420
$!

It expands to the PID of the last background job:

$ ps aux | grep bash &
[1] 663
$ user2      420  0.0  0.4  21156  5012 pts/0    Ss+  17:10   0:00 -bash
user2      663  0.0  0.0  12784   972 pts/0    S    18:08   0:00 grep bash
^C
[1]+  Done                   ps aux | grep bash
$ echo $!
663
Note

Remember, the ampersand (&) is used to start processes in the background.

Positional parameters $0 through $9

They expand to the parameters or arguments being passed to the function (alias or script) — $0 expanding to the name of the script or shell.

Let us create a function to demonstrate positional parameters — note PS2 (>) indicating new lines after line breaks:

$ special_vars() {
> echo $0
> echo $1
> echo $2
> echo $3
}

Now, we will invoke the function (special_vars) passing three parameters to it (debian, ubuntu, zorin):

$ special_vars debian ubuntu zorin
-bash
debian
ubuntu
zorin

It worked as expected.

Warning

Although passing positional parameters to aliases is technically possible, it is not at all functional since — with aliases — positional parameters are always passed at the end:

$ alias great_editor='echo $1 is a great text editor'
$ great_editor emacs
is a great text editor emacs

Other Bash special built-in variables include:

$#

It expands to the number of arguments passed to the command.

$@, $*

They expand to the arguments passed to the command.

$_

It expands to the last parameter or the name of the script (amongst other things; see man bash to find out more!):

Variables in Functions

Of course, variables can be used within functions.

To prove it, this time we will create a new empty file called funed and put the following function into it:

editors() {

editor=emacs

echo "My editor is: $editor. $editor is a fun text editor."
}

As you may have guessed by now, we must source the file first to be able to invoke the function:

$ . funed

And now we can test it:

$ editors
My editor is emacs. emacs is a fun text editor.

As you can appreciate, for the editors function to work properly, the editor variable must first be set. The scope of that variable is local to the current shell and we can reference it as long as the session lasts:

$ echo $editor
emacs

Together with local variables we can also include environment variables in our function:

editors() {

editor=emacs

echo "The text editor of $USER is: $editor."
}

editors

Note how this time we decided to call the function from within the file itself (editors in the last line). That way, when we source the file, the function will also be invoked — all at once:

$ . funed
The text editor of user2 is: emacs.

Positional Parameters in Functions

Something similar occurs with positional parameters.

We can pass them to functions from within the file or script (note the last line: editors tortoise):

editors() {

editor=emacs

echo "The text editor of $USER is: $editor."
echo "Bash is not a $1 shell."
}

editors tortoise

We source the file and prove it works:

$ . funed
The text editor of user2 is: emacs.
Bash is not a tortoise shell.

And we can also pass positional parameters to functions at the command line. To prove it, we get rid of the last line of the file:

editors() {

editor=emacs

echo "The text editor of $USER is: $editor."
echo "Bash is not a $1 shell."
}

Then, we have to source the file:

$ . funed

Finally, we invoke the function with tortoise as the positional parameter $1 at the command line:

$ editors tortoise
The text editor of user2 is: emacs.
Bash is not a tortoise shell.

Functions in Scripts

Functions are mostly found in Bash scripts.

Turning our funed file into a script (we will name it funed.sh) is really a piece of cake:

#!/bin/bash

editors() {

editor=emacs

echo "The text editor of $USER is: $editor."
echo "Bash is not a $1 shell."
}

editors tortoise

That is it! We only added two lines:

  • The first line is the shebang and defines what program is going to interpret the script: #!/bin/bash. Curiously enough, that program is bash itself.

  • The last line is simply the invocation of the function.

Now there is only one thing left — we have to make the script executable:

$ chmod +x funed.sh

And now it is ready to be executed:

$ ./funed.sh
The text editor of user2 is: emacs.
Bash is not a tortoise shell.
Note

You will learn all about shell scripting in the next few lessons.

A Function within an Alias

As said above, we can put a function inside an alias:

$ alias great_editor='gr8_ed() { echo $1 is a great text editor; unset -f gr8_ed; }; gr8_ed'

This long alias value deserves an explanation. Let us break it down:

  • First there is the function itself: gr8_ed() { echo $1 is a great text editor; unset -f gr8_ed; }

  • The last command in the function — unset -f gr8_ed — unsets the function so that it does not remain in the present bash session after the alias is called.

  • Last but not least, to have a successful alias invocation, we must first invoke the function too: gr8_ed.

Let us invoke the alias and prove it works:

$ great_editor emacs
emacs is a great text editor

As shown in unset -f gr8_ed above, the unset command is not only used to unset variables, but also functions. In fact, there are specific switches or options:

unset -v

for variables

unset -f

for functions

If used without switches, unset will try to unset a variable first and — if it fails — then it will try to unset a function.

A Function within a Function

Now say we want to communicate two things to user2 every time she logs into the system:

  • Say hello and recommend/praise a text editor.

  • Since she is starting to put a lot of Matroska video files in its $HOME/Video folder, we also want to give her a warning.

To accomplish that purpose, we have put the following two functions into /home/user2/.bashrc:

The first function (check_vids) does the checking on .mkv files and the warning:

check_vids() {
    ls -1 ~/Video/*.mkv > /dev/null 2>&1
    if [ "$?" = "0" ];then
        echo -e "Remember, you must not keep more than 5 video files in your Video folder.\nThanks."
    else
	echo -e "You do not have any videos in the Video folder. You can keep up to 5.\nThanks."
    fi
}

check_vids does three things:

  • It lists the mkv files in ~/Video sending the output — and any errors — to the so-called bit-bucket (/dev/null).

  • It tests the output of the previous command for success.

  • Depending on the result of the test, it echoes one of two messages.

The second function is a modified version of our editors function:

editors() {

editor=emacs

echo "Hi, $USER!"
echo "$editor is more than a text editor!"

check_vids
}

editors

It is important to observe two things:

  • The last command of editors invokes check_vids so both functions get chained: The greeting, praise and the checking and warning are executed in sequence.

  • editors itself is the entry point to the sequence of functions so it is invoked in the last line (editors).

Now, let us log in as user2 and prove it works:

# su - user2
Hi, user2!
emacs is more than a text editor!
Remember, you must not keep more than 5 video files in your Video folder.
Thanks.

Guided Exercises

  1. Complete the table with “Yes” or “No” considering the capabilities of aliases and functions:

    Feature Aliases? Functions?

    Local variables can be used

    Environment variables can be used

    Can be escaped with \

    Can be recursive

    Very productive when used with positional parameters

  2. Enter the command that list all aliases in your system:

  3. Write an alias named logg that lists all ogg files in ~/Music — one per line:

  4. Invoke the alias to prove it works:

  5. Now, modify the alias so that it echoes out the session’s user and a colon before the listing:

  6. Invoke it again to prove this new version also works:

  7. List all aliases again and check your logg alias appears in the listing:

  8. Remove the alias:

  9. Study the columns “Alias Name” and “Aliased Command(s)`” and assign the aliases to their values correctly:

    Alias Name Aliased Command(s) Alias Assignment

    b

    bash

    bash_info

    which bash + echo "$BASH_VERSION"

    kernel_info

    uname -r

    greet

    echo Hi, $USER!

    computer

    pc=slimbook + echo My computer is a $pc

  10. As root, write a function called my_fun in /etc/bash.bashrc. The function must say hello to the user and tell them what their path is. Invoke it so that the user gets both messages every time they log in:

  11. Login as user2 to check it works:

  12. Write the same function in just one line:

  13. Invoke the function:

  14. Unset the function:

  15. This is a modified version of the special_vars function:

    $ special_vars2() {
    > echo $#
    > echo $_
    > echo $1
    > echo $4
    > echo $6
    > echo $7
    > echo $_
    > echo $@
    > echo $?
    > }

    This is the command we use to invoke it:

    $ special_vars2 crying cockles and mussels alive alive oh

    Guess the outputs:

    Reference Value

    echo $#

    echo $_

    echo $1

    echo $4

    echo $6

    echo $7

    echo $_

    echo $@

    echo $?

  16. Based on the sample function (check_vids) in the section “A Function within a Function”, write a function named check_music to include into a bash startup script that accepts positional parameters so that we can modify easily:

    • the type of file being checked: ogg

    • the directory in which files are saved: ~/Music

    • the type of file being kept: music

    • the number of files being saved: 7

Explorational Exercises

  1. Read-only functions are those whose contents we cannot modify. Do a research on readonly functions and complete the following table:

    Function Name Make it readonly List all readonly Functions

    my_fun

  2. Search the web for how to modify PS1 and anything else you may need to write a function called fyi (to be placed in a startup script) which gives the user the following information:

    • name of the user

    • home directory

    • name of the host

    • operating system type

    • search path for executables

    • mail directory

    • how often mail is checked

    • how many shells deep the current session is

    • prompt (you should modify it so that it shows <user>@<host-date>)

Summary

In this lesson you learned:

  • Both aliases and functions are important features of the shell that allow us to encapsulate recurrent blocks of code.

  • Aliases are useful to have shorter versions of long and/or complicated commands.

  • Functions are procedures that implement logic and allow us to automate tasks, specially when used in scripts.

  • The syntax to write aliases and functions.

  • How to concatenate various commands by means of the semicolon (;).

  • How to properly use quotes with aliases.

  • How to make aliases and functions persistent.

  • Bash special built-in variables: $?, $$, $!, positional parameters ($0-$9), $#, $@, $* and $_.

  • How to use variables and positional parameters with functions.

  • How to use functions in scripts.

  • How to invoke a function from an alias.

  • How to invoke a function from another function.

  • The basics for creating a bash script.

Commands and keywords used in this lesson:

alias

Create aliases.

unalias

Remove aliases.

cd

Change directory.

grep

Print lines matching a pattern.

function

Shell keyword to create functions.

.

Source a file.

source

Source a file.

ps

Report a snapshot of the current processes.

echo

Display a line of text.

chmod

Change mode bits of a file, for example make it executable.

unset

Unset variables and functions.

su

Change user ID or become superuser.

Answers to Guided Exercises

  1. Complete the table with “Yes” or “No” considering the capabilities of aliases and functions:

    Feature Aliases? Functions?

    Local variables can be used

    Yes

    Yes

    Environment variables can be used

    Yes

    Yes

    Can be escaped with \

    Yes

    No

    Can be recursive

    Yes

    Yes

    Very productive when used with positional parameters

    No

    Yes

  2. Enter the command that list all aliases in your system:

    alias
  3. Write an alias named logg that lists all ogg files in ~/Music — one per line:

    alias logg='ls -1 ~/Music/*ogg'
  4. Invoke the alias to prove it works:

    logg
  5. Now, modify the alias so that it echoes out the session’s user and a colon before the listing:

    alias logg='echo $USER:; ls -1 ~/Music/*ogg'
  6. Invoke it again to prove this new version also works:

    logg
  7. List all aliases again and check your logg alias appears in the listing:

    alias
  8. Remove the alias:

    unalias logg
  9. Study the columns “Alias Name” and “Aliased Command(s)” and assign the aliases to their values correctly:

    Alias Name Aliased Command(s) Alias Assignment

    b

    bash

    alias b=bash

    bash_info

    which bash + echo "$BASH_VERSION"

    alias bash_info='which bash; echo "BASH_VERSION"'

    kernel_info

    uname -r

    alias kernel_info='uname -r'

    greet

    echo Hi, $USER!

    alias greet='echo Hi, $USER'

    computer

    pc=slimbook + echo My computer is a $pc

    alias computer='pc=slimbook; echo My computer is a $pc'

    Note

    Single quotes can also be replaced by double ones.

  10. As root, write a function called my_fun in /etc/bash.bashrc. The function must say hello to the user and tell them what their path is. Invoke it so that the user gets both messages every time they log in:

    Option A:

    my_fun() {
    echo Hello, $USER!
    echo Your path is: $PATH
    }
    my_fun

    Option B:

    function my_fun {
    echo Hello, $USER!
    echo Your path is: $PATH
    }
    my_fun
  11. Login as user2 to check it works:

    su - user2
  12. Write the same function in just one line:

    Option A:

    my_fun() { echo "Hello, $USER!"; echo "Your path is: $PATH"; }

    Option B:

    function my_fun { echo "Hello, $USER!"; echo "Your path is: $PATH"; }
  13. Invoke the function:

    my_fun
  14. Unset the function:

    unset -f my_fun
  15. This is a modified version of the special_vars function:

    $ special_vars2() {
    > echo $#
    > echo $_
    > echo $1
    > echo $4
    > echo $6
    > echo $7
    > echo $_
    > echo $@
    > echo $?
    > }

    This is the command we use to invoke it:

    $ special_vars2 crying cockles and mussels alive alive oh

    Guess the outputs:

    Reference Value

    echo $#

    7

    echo $_

    7

    echo $1

    crying

    echo $4

    mussels

    echo $6

    alive

    echo $7

    oh

    echo $_

    oh

    echo $@

    crying cockles and mussels alive alive oh

    echo $?

    0

  16. Based on the sample function (check_vids) in section “A Function within a Function”, write a function named check_music to include into a bash startup script that accepts positional parameters so that we can modify easily:

    • the type of file being checked: ogg

    • the directory in which files are saved: ~/Music

    • the type of file being kept: music

    • the number of files being saved: 7

      check_music() {
          ls -1 ~/$1/*.$2 > ~/.mkv.log 2>&1
          if [ "$?" = "0" ];then
              echo -e "Remember, you must not keep more than $3 $4 files in your $1 folder.\nThanks."
          else
      	 echo -e "You do not have any $4 files in the $1 folder. You can keep up to $3.\nThanks."
          fi
      }
      
      check_music Music ogg 7 music

Answers to Explorational Exercises

  1. Read-only functions are those whose contents we cannot modify. Do a research on readonly functions and complete the following table:

    Function Name

    Make it readonly

    List all readonly Functions

    my_fun

    readonly -f my_fun

    readonly -f

  2. Search the web for how to modify PS1 and anything else you may need to write a function called fyi (to be placed in a startup script) which gives the user the following information:

    • name of the user

    • home directory

    • name of the host

    • operating system type

    • search path for executables

    • mail directory

    • how often mail is checked

    • how many shells deep the current session is

    • prompt (you should modify it so that it shows <user>@<host-date>)

      fyi() {
          echo -e "For your Information:\n
          Username: $USER
          Home directory: $HOME
          Host: $HOSTNAME
          Operating System: $OSTYPE
          Path for executable files: $PATH
          Your mail directory is $MAIL and is searched every $MAILCHECK seconds.
          The current level of your shell is: $SHLVL"
          PS1="\u@\h-\d "
      }
      
      fyi

© 2020 Linux Professional Insitute Inc. All rights reserved. Visit the Learning Materials website: https://learning.lpi.org
This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

Next Lesson

105.2 Customize or write simple scripts (105.2 Lesson 1)

Read next lesson

© 2020 Linux Professional Insitute Inc. All rights reserved. Visit the Learning Materials website: https://learning.lpi.org
This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

LPI is a non-profit organization.

Linux Professional Institute (LPI) is the global certification standard and career support organization for open source professionals. With more than 200,000 certification holders, it's the world’s first and largest vendor-neutral Linux and open source certification body. LPI has certified professionals in over 180 countries, delivers exams in multiple languages, and has hundreds of training partners.

Our purpose is to enable economic and creative opportunities for everybody by making open source knowledge and skills certification universally accessible.

  • LinkedIn
  • flogo-RGB-HEX-Blk-58 Facebook
  • Twitter
  • Contact Us
  • Privacy and Cookie Policy

Spot a mistake or want to help improve this page? Please let us know.

© Copyright 1999-2020 The Linux Professional Institute Inc. All rights reserved.