This is a riveting series:
- On the LPIC-1 Exam 102: Shells and Shell Scripting
- On the LPIC-1 Exam 102: User Interfaces and Desktops
- On the LPIC-1 Exam 102: Administrative Tasks
- On the LPIC-1 Exam 102: Essential System Services
- On the LPIC-1 Exam 102: Networking Fundamentals
- On the LPIC-1 Exam 102: Security
And, so is this one!
When studying for the Linux Professional Institute LPIC-1 certification, I took a bunch of notes when reading the docs and doing an online course. Note that this is not exhaustive, so do not depend on this article to get you prepared for the exam.
The menu items below are not in any order.
This roughly covers Topic 105: Shells and Shell Scripting.
Caveat emptor.
- Exam Details
- Topic 105: Shells and Shell Scripting
- Summary
- References
Exam Details
- Exam Objectives Version: 5.0
- Exam Code: 102-500
Topic 105: Shells and Shell Scripting
Terminals
tty- tele-typewriterpts- pseudo-terminal slave
Shell Types
A great way to see which initialization files are sourced by bash for a particular invocation of a shell is to append an echo statement to each global file:
echo "echo hello from /etc/profile" | sudo tee -a /etc/profile
echo "echo hello from /etc/bash.bashrc" | sudo tee -a /etc/bash.bashrc
Interactive Login
Examples include:
- Invoke a login shell:
bash -lorbash -login
- Drop to a
ttyon your local machine.Ctrl+Alt+F2, for example
- Logging into a remote machine via
ssh.
The following files are sourced, in order:
/etc/profile- the system-wide profile file for the Bourne shell and Bourne compatible shells
- sets
PATHandPS1
/etc/bash.bashrc- if they exist, scripts in
/etc/profile.d/get executed by/etc/profile $HOME/{.bash_profile,.bash_login,.profile}- this will
source$HOME/.bashrc - appends
$HOME/bintoPATH
- this will
$HOME/.bash_logout
To start an interactive login shell that doesn’t source any configuration files, use the
--noprofileswitch (will still source/etc/bash.bashrcand$HOME/.bashrc, though).So, it appears to behave like an interactive non-login shell.
Interactive Non-Login
A general definition of an interactive shell is one that reads from and writes to a user’s terminal:
An interactive shell is one started without non-option arguments (unless
-sis specified) and without specifying the-coption, whose input and error output are both connected to terminals (as determined byisatty(3)), or one started with the -i option.
Here are some fine examples of interactive non-login shells:
-
Subshell:
$ bash -
Subshell with
-i(interactive) switch:$ bash -i -
With the
-sswitch and positional parameters:$ cat test.sh #!/usr/bin/bash echo "Today's message is: $1 $2" $ < test.sh bash -s -- hello world Today's message is: hello world $ cat test.sh | bash -s -- hello world Today's message is: hello worldNote that the second example suffers from
UUOC.
The following files are sourced, in order:
/etc/bash.bashrc$HOME/.bashrc
Optionally, you can use the --rcfile option to have bash skip any initialization from the user $HOME/.bashrc file and instead configure the shell’s environment from the file specified as its value:
$ bash --rcfile alternate.bashrc
This will still source the system-wide /etc/bash.bashrc file (at least, it did on my machine using Debian bullseye).
To start an interactive non-login shell that doesn’t source any configuration files, use the
--norcswitch.
Non-Interactive Login
These are rare.
Some examples:
/bin/bash --login <some_script><some_command> | ssh <some_user>@<some_server>
Non-Interactive Non-Login
Running a script will give you a bash shell of this type. Every script will run in its own subshell, opening the shell on execution and closing it on exit.
Here’s an interesting property of these types of shells. There is a variable used by the shell that’s named BASH_ENV, and its purpose is to contain a filename that should be sourced to initialize the shell.
Recall that non-interactive non-login shells will not have any initialization files run by the shell when it’s launched to customize its environment, because these types of shells are primarily for shells running scripts.
So, if you need the shell to have a custom environment, put it in BASH_ENV. Note, however, that it should be an absolute path to the file because the shell doesn’t consult the PATH variable for any lookups.
Here’s an example that’s currently running in production at benjamintoll.com:
scripting.rc
$ cat scripting.rc
export COOTIES="yes, i have them"
c.sh
#!/bin/bash
test -n "$COOTIES" && echo "$COOTIES"
Let’s first run without setting the variable:
$ ./c.sh
$
Now, let’s augment the subshell’s environment:
$ env BASH_ENV=$(pwd)/scripting.rc ./c.sh
yes, i have them
weeeeeeeeeeeeeeeeeee
Determine the Shell Type
To determine what type of shell you have, use the special parameter $0 or $BASH_ARGV0. If the result is prepended by a hyphen (-), then it’s a login shell. If it prints bash without a leading hyphen, then it’s not:
$ echo $0
-bash
In addition, there is the Bash specific, non-POSIX login_shell option.
Let’s take a look at two examples. First, an interactive login shell, and second, an interactive non-login shell.
Interactive login shell:
$ echo $0
-bash
$ shopt login_shell
login_shell on
$ echo $-
himBCHs
Interactive non-login shell:
$ bash
$ echo $0
bash
$ shopt login_shell
login_shell off
$ echo $-
himBCHs
The special parameter
$-displays the shell options that have been set for the session. Since both of the previous examples are interactive, they both report the-iswitch.
Interestingly, the ${PS1-} bash shell variable will also tell you if the shell is interactive or not:
$ echo ${PS1-}
$(tput bold)$(tput setaf 4)\h $(tput setaf 2)|$SHLVL:$0| $(tput setaf 3)~~> \[$(tput bold)\]\[$(tput setaf 6)\]\w\[\]:\[$(tput bold)\]\[$(tput setaf 8)\]$(git branch 2> /dev/null | grep "^*" | colrm 1 2)\[$(tput sgr0)\]\n$
However, if you run it in a script, it won’t echo anything. This is another way of demonstrating that running bash shell scripts are non-interactive non-login.
Note that the
PS*variables are shell variables and not environment variables. In other words, they are explicitly set by a script and are not inherited by any child processes.For instance, you’ll never see any
PS*variables exported.
I stumbled across this when viewing the system’s /etc/profile script:
$ head -25 /etc/profile
# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
# and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).
if [ "$(id -u)" -eq 0 ]; then
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
else
PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
fi
export PATH
if [ "${PS1-}" ]; then
if [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then
# The file bash.bashrc already sets the default PS1.
# PS1='\h:\w\$ '
if [ -f /etc/bash.bashrc ]; then
. /etc/bash.bashrc
fi
else
if [ "$(id -u)" -eq 0 ]; then
PS1='# '
else
PS1='$ '
Interesting, no?
Also, notice that the PS1 shell variable is explicitly set.
su and sudo
With su, you can run a command with substitute user and group ID or become the superuser root. Both login and non-login shells can be invoked.
For example:
- to start an interactive login shell as
kilgore:su - kilgoresu -l kilgoresu --login kilgore
- to start an interactive non-login shell as
kilgore:su kilgore
- to start an interactive login shell as
root:su - rootsu -
- to start an interactive non-login shell as
root:su rootsu
With sudo, you execute command(s) as another user, commonly root. The default sudo security policy plugin sudoers is driven by the /etc/sudoers file, which determines a user’s sudo privileges.
This is especially useful when you can’t login as the root account.
It is important to only modify the
/etc/sudoersfile through thevisudoutility, which will lock the file to prevent concurrent writes.
As with su, sudo allows you to create both login and non-login shells.
For example:
- to start an interactive login shell as
kilgore:sudo su - kilgoresudo su -l kilgoresudo su --login kilgore
- to start an interactive non-login shell as
kilgore:sudo su kilgoresudo -u kilgore -s- If no command is specified for the
-s(shell) switch, an interactive shell is executed.
- If no command is specified for the
- to start an interactive login shell as
root:sudo su - rootsudo su -sudo -i- The
-iswitch tells it to run as an interactive login shell.
- The
- to start an interactive login shell as
rootand return to the calling user after execution of the command:sudo -i <some_command>- The
-iswitch tells it to run as an interactive login shell.
- The
- to start an interactive non-login shell as
root:sudo su rootsudo susudo -ssudo -u root -s- If no command is specified for the
-s(shell) switch, an interactive shell is executed.
- If no command is specified for the
The decision to use a login or non-login shell comes down to the need and the use case. If you want to have /etc/profile executed and anything else that it sources (such as anything in /etc/profile.d/ and /etc/.bash_profile in the user’s home directory), then choose an option that invokes a login shell.
What’s the difference between
sudo su -andsu -?With
sudo su -you will be asked to authenticate with your own user password, while you will be expected to know the root user’s password for the latter command.
Recall that a login shell will start sourcing with the system-wide /etc/profile file, while a non-login shell will not, only starting its sourcing at the system-wide /etc/bash.bashrc file.
Add the
kilgoreuser to thesudogroup by issuing the following command:$ sudo usermod -aG sudo kilore
SKEL
If set, the SKEL environment variable holds the location of the skel directory. This is the location that holds the (probably hidden) files that are copied to every user’s home directory when their account is created. They must be a regular user (as opposed to a system account) that needs a home directory and is specified by the system administrator at the time the user account is created (keep in mind that not every new account creation needs a home directory, like system accounts).
SKEL is defined in /etc/adduser.conf (default values are in /etc/default/useradd):
$ grep SKEL /etc/adduser.conf
# The SKEL variable specifies the directory containing "skeletal" user
SKEL=/etc/skel
# If SKEL_IGNORE_REGEX is set, adduser will ignore files matching this
SKEL_IGNORE_REGEX="dpkg-(old|new|dist|save)"
The location of
SKELcan be changed on user account creation by the value of the-kor--skeloption (as mentioned earlier, only applicable if-mor--create-homeis specified).
Here are the default contents on my Debian bullseye machine:
$ ls -A /etc/skel/
.bash_logout .bashrc .profile
Any directories and files created in this location (or any location specified in the -k option to useradd) will also get these custom entries when their account is created with a home directory.
Why is this environment variable (probably) not set in your environment? Because the env var is only created when an account is created and doesn’t outlive the subprocess.
Shell Variables
Variables can contain the following characters:
[a-z][A-Z][0-9]_(underscore)
They cannot start with a number.
Shell variables can be created as immutable by prefacing it with the readonly builtin (inherited from the Bourne shell):
$ readonly fudge=original
$ fudge=changed
-bash: fudge: readonly variable
Or, mark it as such after creation:
$ fudge=original
$ readonly fudge
Print all read-only variables by invoking either
readonlyorreadonly -p.
You can also create a read-only variable using the declare builtin:
$ declare -r chicken=maybe
$ echo $chicken
maybe
$ chicken=never
-bash: chicken: readonly variable
Variables become environment variables by exporting them. They are then part of the environment that is inherited by subshells.
Turn an environment variable back into a local variable:
$ export TICKLE=my_fancy
$ bash
$ echo $TICKLE
my_fancy
$ exit
$ export -n TICKLE
$ bash
$ echo $TICKLE
$
exportorexport -pwill print all environment variables.
To turn the TICKLE local variable from the previous example back into an environment variable, you can use either of the following commands:
$ export TICKLE
$ declare -x TICKLE
Quoting
Single and double-quotes are not interchangeable (however, they sometimes are equivalent in what they do, i.e., ‘i am a string with no special characters’ == “i am a string with no special characters”).
Follow these rules, and you’ll be as safe as houses:
- single quotes are for literal interpretation
- double quotes allow for variable substitution
Let’s see some examples:
$ maga="make attorneys get attorneys"
$ echo '$maga'
$maga
$ echo "$maga"
make attorneys get attorneys
In addition, you’ll want to use double-quotes to avoid word splitting and pathname expansion:
$ hello="| hello, it's me |"
$ echo '$hello'
$hello
$ echo $hello
| hello, it's me |
$ echo "$hello"
| hello, it's me |
It’s almost always a good idea to double-quote any variables in scripts so variable substitution occurs and word splitting is avoided. Further, if a variable is empty, double-quoting will prevent a syntax error when evaluated.
Running a Program in a Modified Environment
To start a subshell with a limited environment, preface the shell command with env and the -i or --ignore-environment switch:
$ env -i bash
$ env
PWD=/home/btoll/projects/benjamintoll.com
LS_COLORS=
LESSCLOSE=/usr/bin/lesspipe %s %s
LESSOPEN=| /usr/bin/lesspipe %s
SHLVL=1
_=/usr/bin/env
You can also use the
--ignore-environmentoption and also just-, which implies-i.
You can also augment the environment inherited by a subshell by prefacing the bash command again with env, but this time specifying the new variables to inherit:
$ env ZOMBIES=are_real ./run_away.sh
You can omit the
envcommand in the previous example, as it’s implied.
Common Environment Variables
DISPLAYHISTCONTROLignorespace- don’t save commands starting with a space
ignoredups- repeated, consecutive commands will not be saved
ignoreboth- a command that falls into the previous two categories won’t be saved
HISTSIZEHISTFILESIZEHISTFILE- defaults to
.bash_history
- defaults to
HOMEHOSTNAMEHOSTTYPE- the CPU architecture (
x86_64)
- the CPU architecture (
LANG- the locale of the system
LD_LIBRARY_PATH- the location of shared libraries
MAILMAILCHECK- the frequency in seconds that mail is checked
PATHPS1- the prompt
PS2- the continuation prompt for multi-line commands (defaults to
>)
- the continuation prompt for multi-line commands (defaults to
PS3- the
selectcommand prompt
- the
PS4- for debugging (defaults to
+)
- for debugging (defaults to
SHELLUSER
The following commands will all print the shell’s environment variables:
exportprintenvenv
Sourcing
Sourcing a bash script will not create a subprocess. This means that all of the commands will execute within the context (i.e,. environment) of the current shell.
Why is this useful? Well, this method is used a lot when creating environment variables that are needed for a particular session. By sourcing the file instead of executing it, when the file is done being read the variables are still part of the session, whereas they wouldn’t be if executing the file (recall that a child cannot modify a parent’s environment).
Seeing this in action will make more sense.
The first thing we’ll do is show the current values of the number of bash instances, the PID of the current bash process and the status of the FOO environment variable:
$ echo $$ ; echo $SHLVL ; if [ -z $FOO ]; then echo unset; else echo set; fi
25350
3
unset
Here is our little source.sh bash script:
#!/bin/bash
echo "Number of bash instances: $SHLVL"
echo "PID of current bash instance: $$"
FOO=bar
First, let’s source it:
$ . source.sh
Number of bash instances: 3
PID of current bash instance: 25350
Ok, this demonstrates that the script did indeed run in the same shell. The number of bash instances is the same (as shown by the value of the SHLVL variable), as well as the PID.
Let’s see if the FOO variable is on the stack:
$ echo $FOO
bar
Kool Moe Dee.
Before we execute the script, unset that variable:
$ unset FOO ; if [ -z $FOO ]; then echo unset; else echo set; fi
unset
Second, we’ll execute the same file:
$ bash source.sh
Number of bash instances: 4
PID of current bash instance: 35246
It (the subprocess) created another bash instance, thus incrementing SHLVL as we’d expect. Also, the PID is that of the subprocess.
Did it set the FOO variable in the current shell?
$ if [ -z $FOO ]; then echo unset; else echo set; fi
unset
Nope.
Weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
It’s important to know when to use the
exportkeyword when sourcing a script and when it’s not necessary.For instance, if a script is sourcing another shell script that has several variables defined, then it’s not necessary to preface each variable with the keyword
export(thus, turning them into environment variables) if they are only to be used in the single script (i.e., the script that is sourcing).However, if the script that is sourcing is in turn calling other scripts that are expecting to use the variables, then the must be environment variables, so the keyword
exportis mandatory. This, of course, is how subprocesses inherit its parent’s environment.
Aliases
To unmask a command that has been masked by an alias, preface the command with a backslash (\l):
$ alias ls
alias ls='ls -F'
$ ls
archetypes/ config.toml Dockerfile k8s/ public/ resources/
build_and_deploy.sh* content/ env.sh* Makefile README.md static/
$ \ls
archetypes config.toml Dockerfile k8s public resources
build_and_deploy.sh content env.sh Makefile README.md static
Note that the ls alias has added the -F to classify the directory entries (this appends the indicators to the appropriate entries). This is a common alias that is provided out-of-the-box, so to speak, by many distributions.
Use the backslash to temporarily “turn off” (i.e., just for that command) the alias to allow the original command, now unshadowed, to run.
However, if there is no underlying command that is being shadowed, you will get an error. Observe:
$ type xkcd
xkcd is aliased to `open https://c.xkcd.com/random/comic/'
$ alias xkcd
alias xkcd='open https://c.xkcd.com/random/comic/'
$ xkcd
( opened the page in a browser )
$ \xkcd
-bash: xkcd: command not found
If for some reason you wish to remove all of the aliases from your session:
$ unalias -a
Let’s take a look at the difference between single and double quotes in the definition of an alias.
With single quotes, the shell variable expansion is dynamic:
$ alias you_are_here='echo $PWD'
$ you_are_here
/home/btoll/projects/benjamintoll.com
$ cd
$ you_are_here
/home/btoll
Now, redefine the alias using double quotes. Notice now that the expansion is static and will always refer to the location in which the alias was defined:
$ alias you_are_here="echo $PWD"
$ you_are_here
/home/btoll
$ cd -
/home/btoll/projects/benjamintoll.com
$ !-2
you_are_here
/home/btoll
Functions
Syntax can be either:
function FUNC_NAME {
...
}
or:
FUNC_NAME() {
...
}
To create local variables scoped to the function:
local VARdeclare -A|-a|-i|-n VAR
Otherwise, the variable will be global and then part of the calling environment.
Let’s see an example of this.
When I was a history major at university, my ancient Roman professor introduced us to a Greek warrior that fought the Romans. His name was Testicles (pronounced test-ee-klees).
Let’s dedicate this bash function to Testicles.
Here is its definition:
$ type testicles
testicles is a function
testicles ()
{
local roman=0;
greek=1
}
Note that there is a roman variable prefaced by the local keyword and a greek variable that is not.
And now we’ll use an interactive login shell to test the known variables before and after explicitly calling the function:
So far, neither variable is known to the shell.
$ set | grep -E "^(roman|greek)"
$ echo $roman
$ echo $greek
Now, we’ll call the function and see if anything has changed:
$ testicles
$ echo $roman
$ echo $greek
1
$ set | grep -E "^(roman|greek)"
greek=1
Uh oh, the greek variable, unprefaced by the local keyword and thus a non-local (global) variable, is now present in the shell. This is not good.
Always use local in your bash functions, children.
Builtin Variables
These special variables are known as parameters:
| Variable | Description |
|---|---|
$? |
the return value of the last command |
$$ |
the PID of the shell |
$! |
the PID of the last background job |
$0-$9 |
positional parameters |
$# |
the number of arguments passed to the command |
$@ or $* |
the arguments passed to the command |
$_ |
the last parameter or the name of the script |
$- |
the shell options that have been set for the session |
Unsetting
unset -v- for variables
unset -f- for functions
unset(with no option)- first tries to unset as a variable and then failing that as a function
Testing
It is highly recommended to use double quotes around any variables in case the variable is empty, in which case any command expecting an operand would throw an error without the double quotes.
Variables
One operand:
-n- the length ofSTRINGis nonzero-z- the length ofSTRINGis zero
Two operands:
| Operands | Description |
|---|---|
STRING1 = STRING2 |
the strings are equal (can also use double equal sign ==) |
STRING1 != STRING2 |
the strings are not equal |
INTEGER1 -eq INTEGER2 |
INTEGER1 is equal to INTEGER2 |
INTEGER1 -ge INTEGER2 |
INTEGER1 is greater than or equal to INTEGER2 |
INTEGER1 -gt INTEGER2 |
INTEGER1 is greater than INTEGER2 |
INTEGER1 -le INTEGER2 |
INTEGER1 is less than or equal to INTEGER2 |
INTEGER1 -lt INTEGER2 |
INTEGER1 is less than INTEGER2 |
INTEGER1 -ne INTEGER2 |
INTEGER1 is not equal to INTEGER2 |
Distinct languages may have different rules for alphabetical ordering. To obtain consistent results, regardless of the localization settings of the system where the script is being executed, it is recommended to set the environment variable
LANGtoC, as inLANG=C, before doing operations involving alphabetical ordering.
Files
Here are two utilities for testing at the terminal. However, they’re mostly used in scripts:
test- [ (is a synonym of
test)Yes, that’s right, the character above is an open bracket (
[):$ which [ /usr/bin/[It’s mostly seen in conditional checks in shell scripts, i.e.,
if [ -d /etc/squid ] then ...weeeeeeeeeeeeeeeeeeee
Also, see this astounding article on testing that will leave you wanting more.
One operand:
| Operand | Description |
|---|---|
-a |
FILE exists |
-b |
FILE exists and is block special |
-c |
FILE exists and is character special |
-d |
FILE exists and is a directory |
-e |
FILE exists |
-f |
FILE exists and is a regular file |
-g |
FILE exists and is set-group-ID |
-G |
FILE exists and is owned by the effective group ID |
-h |
FILE exists and is a symbolic link (same as -L) |
-k |
FILE exists and has its sticky bit set |
-L |
FILE exists and is a symbolic link (same as -h) |
-N |
FILE exists and has been modified since it was last read |
-O |
FILE exists and is owned by the effective user ID |
-p |
FILE exists and is a named pipe |
-r |
FILE exists and read permission is granted |
-s |
FILE exists and has a size greater than zero |
-S |
FILE exists and is a socket |
-t |
FD is opened on a terminal |
-u |
FILE exists and its set-user-ID bit is set |
-w |
FILE exists and write permission is granted |
-x |
FILE exists and execute (or search) permission is granted |
Two operands:
| Operands | Description |
|---|---|
FILE1 -ef FILE2 |
FILE1 and FILE2 have the same device and inode numbers |
FILE1 -nt FILE2 |
FILE1 is newer (modification date) than FILE2 |
FILE1 -ot FILE2 |
FILE1 is older than FILE2 |
Is executable?
$ [ -x /bin/bash ]
$ echo $?
0
$ test -x /bin/bash
$ echo $?
0
Is a soft link?
$ test -L /lib64/ld-linux-x86-64.so.2
$ echo $?
0
Is present and is a directory?
$ [ -d /etc ] ; echo $?
0
$ [ -d /etcy ] ; echo $?
1
Expressions
( EXPRESSION )-EXPRESSIONis true! EXPRESSION-EXPRESSIONis falseEXPRESSION1 -a EXPRESSION2- bothEXPRESSION1andEXPRESSION2are trueEXPRESSION1 -o EXPRESSION2- eitherEXPRESSION1orEXPRESSION2is true
set -o vs shopt
So, what is the difference between set -o and shopt, anyway?
set -o options are those inherited from Bourne-style shells like ksh, and the shopt options are those specific to `bash.
The help information for shopt is quite telling:
$ help shopt
shopt: shopt [-pqsu] [-o] [optname ...]
Set and unset shell options.
Change the setting of each shell option OPTNAME. Without any option
arguments, list each supplied OPTNAME, or all shell options if no
OPTNAMEs are given, with an indication of whether or not each is set.
Options:
-o restrict OPTNAMEs to those defined for use with `set -o'
-p print each shell option with an indication of its status
-q suppress output
-s enable (set) each OPTNAME
-u disable (unset) each OPTNAME
Exit Status:
Returns success if OPTNAME is enabled; fails if an invalid option is
given or OPTNAME is disabled.
Let’s create an interactive non-login subshell. Then, we’ll print the total number of shell options from shopt and the number after its been restricted to just those supported by set -o:
$ bash
$ shopt | wc -l
53
$ shopt -o | wc -l
27
So, it looks like the bash shell has added support for many more shell options that aren’t available in older shells. Probably, it’s important to be aware of this when writing shell scripts, and another reason why a static analysis tool like shellcheck is essential and should absolutely be part of your toolchain.
For example, if you use the pipefail shell option (always a good idea) in your shell scripts but then have it interpreted by the Bourne shell, shellcheck will give you the following error:
In POSIX sh, set option pipefail is undefined.
Of course, I’m using
shellcheckas a Vim plugin because I’m cool as hell.In
.vimrc:call plug#begin('~/.vim/plugged') Plug 'koalaman/shellcheck' call plug#end()
Lastly, you can quickly find out which shell options are enabled, that is, which ones are reported as “on” by shopt.
$ echo $BASHOPTS
autocd:checkjobs:checkwinsize:cmdhist:complete_fullquote:expand_aliases:extquote:force_fignore:globasciiranges:globstar:histappend:hostcomplete:interactive_comments:login_shell:progcomp:promptvars:sourcepath
This is a read-only variable, and each word in the list is a valid argument for the -s option to shopt.
IFS
The IFS environment variable stands for the Internal Field Separator.
It is an array of three whitespace characters:
[SPACE]\t\n
$ echo ${#IFS}
3
$
$ printf '%q' "$IFS"
$' \t\n'
$
$ for v in "$IFS"; do printf '%q' "$v"; done
$' \t\n'
Scripting
In order be able to execute a script using the filename as the argument to the interpreter (i.e., bash foo.sh), the read permission bit must be set. Otherwise, a Permission Denied error is raised:
$ bash hello.sh
hi
$ chmod 200 foo.sh
$ bash foo.sh
bash: foo.sh: Permission denied
To execute the file by its path, the execute permission bit must be enabled.
The following are all equivalent (just pretend the mclovin.sh script already exists):
$ /home/btoll/mclovin.sh
hi
$ $(pwd)/mclovin.sh
hi
$ ./mclovin.sh
hi
However, the script’s directory must be included in the PATH if the pathname is to be omitted, leaving just the file name:
$ mclovin.sh
-bash: mclovin.sh: command not found
Add the directory to the PATH, and you will be golden, perhaps even the golden god.
Local Variables
The scripts also have the same special parameters as listed in the Builtin Variables section, with the same meaning.
Positional parameters are numeric. The first one is the script name ($0 or $BASH_ARGV0). Any numbers greater than nine need to be enclosed in curly brackets, such as ${10}. However, if your shell script is accepting that many parameters, you need to rethink some things.
Reading Input
Getting input from the command-line is easy: use read.
Examples:
$ echo "Do you want to continue (y/n)?"
$ read ANSWER
If you don’t give a variable name, it will default to
REPLY.
$ echo "Type your first name and last name:"
$ read FIRST LAST
This combines getting input from the user and also echoing at the same time:
$ read -p "Type your first name and last name:" FIRST LAST
case
case "$foo" in
debian | ubuntu | mint )
echo -n "uses .deb"
;;
centos | fedora | opensuse )
echo -n "uses .rpm"
;;
*)
echo -n "uses unknown package format"
;;
esac
Note that the items to be matched in a case block can employ command subsitition, parameter and arithmetic expansion, etc.
printf
Many programming languages have a printf function, or something similarly-named that operates like it, that formats and prints data. You will not be surprised that bash has a shell builtin called printf that allows for the same functionality.
Here is an example from the LPIC-1 documentation:
$ OS=$(uname -o)
$ FREE=$(( 1000 * `sed -nre '2s/[^[:digit:]]//gp' < /proc/meminfo` ))
$ MSG='Operating system:\t%s\nUnallocated RAM:\t%d MB\n'
$ printf "$MSG" $OS $(( $FREE / 1024**2 ))
Operating system: GNU/Linux
Unallocated RAM: 19375 MB
printfis short for print formatted.
Random Examples
-
get the length of a variable
OS=$(uname -o) echo "${#OS}" 9 -
create an array (one-dimensional)
declare -a SIZESSIZES=( 5677 77 23 )- this both declares and defines
SIZES=( $(cut -f2 /proc/filesystems | head -3) )- use command substitution
- this works, but it will only create an array of one element
echo "${SIZES[0]}" sysfs tmpfs bdev - to get three elements, use
mapfileor its synonymreadarrayto split at each newline (\n) and generate a full arraymapfile -t SIZES <<< "$(cut -f2 /proc/filesystems | head -3 )" echo "${SIZES[0]}" sysfs
-
access array elements
- get the first element
$SIZES${SIZES[0]}
- change the first element
SIZES[0]=20
- get the length of the third element
${#SIZES[2]}
- get total length
${#SIZES[*]}${#SIZES[@]}
Trying to access a non-existent array element (i.e., out-of-bounds) does not produce an error.
- get the first element
-
arithmetic
Summary
Continue your journey with the second installment in this titillating series, On the LPIC-1 Exam 102: User Interfaces and Desktops.