How To Use Bash Parameter Substitution Like A Pro

The $ character is used for parameter expansion, arithmetic expansion and command substitution. You can use it for manipulating and expanding variables on demands without using external commands such as perl, python, sed or awk. This guide shows you how to use parameter expansion modifiers to transform Bash shell variables for your scripting needs.


You can use variables to store data and configuration options. For example:
Use echo or printf command to display variable value:
echo "$dest"
printf "$dest\n"
The parameter name or symbol such as $dest to be expanded may be enclosed in braces
echo "Value ${dest}"
It is optional but serve to protect the variable to be expanded from characters immediately following it which could be interpreted as part of the name.

1. Setting Up Default Shell Variables Value

The syntax is as follows:


If parameter not set, use defaultValue. In this example, your shell script takes arguments supplied on the command line. You’d like to provide default value so that the most common value can be used without needing to type them every time. If variable $1 is not set or passed, use root as default value for u:


Consider the following example:

echo "Setting php-cgi at ${_jail_dir}..."
# rest of the script ...

You can now run this script as follows:

./ /jail              # <--- set php jail at /jail dir
./ /home/httpd/jail   # <---- set php jail at /home/httpd/jail dir
./                    # <--- set php jail dir at /home/phpcgi (default)

Here is another handy example:

        local d="$1"               # get dir name
        local p=${2:-0755}      # get permission, set default to 0755
        [ $# -eq 0 ] && { echo "$0: dirname"; return; }
        [ ! -d "$d" ] && mkdir -m "$p" -p "$d"
## call it ##
_mkdir "/var/www/php" 0644
_mkdir "/var/www/static" 0666
# set default permissions to 0755 in _mkdir() #
_mkdir "/var/www/static"

Use this substitution for creating failsafe functions and providing missing command line arguments in scripts.

#1.1: Setting Default Values

The syntax is as follows:


The assignment (:=) operator is used to assign a value to the variable if it doesn’t already have one. Try the following examples:

echo "$USER"

Sample outputs:


Now, assign a value foo to the $USER variable if doesn’t already have one:

echo ${USER:=foo}

Sample outputs:


Unset value for $USER:

unset USER
echo "${USER:=foo}"

Sample outputs:


This make sure you always have a default reasonable value for your script.

Tip: ${var:-defaultValue} vs ${var:=defaultValue}

Please note that it will not work with positional parameter arguments:

var=${1:=defaultValue}  ### FAIL with an error cannot assign in this way
var=${1:-defaultValue}    ### Perfect

2. Display an Error Message If $VAR Not Passed

If the variable is not defined or not passed, you can stop executing the Bash script with the following syntax:

${varName?Error varName is not defined}
${varName:?Error varName is not defined or is empty}
${1:?"mkjail: Missing operand"}
MESSAGE="Usage: domainname IPv4"             ### define error message
_domain=${2?"Error: ${MESSAGE}"}  ### you can use $MESSAGE too

This is used for giving an error message for unset parameters. In this example, if the $1 command line arg is not passed, stop executing the script with an error message:

_domain="${1:?Usage: mknginxconf domainName}"

Here is a sample script:

# Purpose: Wrapper script to setup Nginx Load Balancer
# Author: Vivek Gite
_domain="${1:?Usage: mknginxconf domainName}"     ### die if domainName is not passed ####
[ ! -f $_db ] && { echo "$0: Error $_db file not found."; exit 1; }
line=$(grep "^${_domain}" $_db) || { echo "$0: Error $_domain not found in $_db."; exit 2; }
# Get domain config info into 4 fields:
# f1 - Domain Name|
# f2 - IPv4Vip:httpPort:HttpsPort, IPv6Vip:httpPort:HttpsPort|
# f3 - PrivateIP1:port1,PrivateIP2,port2,...PrivateIPN,portN|
# f4 - LB Type (true [round robin] OR false [session])
# -------------------------------------------------------------------------------
read -r f1 f2 f3 f4  <<<"$line"
# Do we want ssl host config too?
set -- $f2
[ "$3" == "443" ] && ssl="true"
# Build query
# Call our master script to setup nginx reverse proxy / load balancer (LB) for given domain name
$_setup "$d" "$ips"

2.1. Display an Error Message and Run Command

If $2 is not set display an error message for $2 parameter and run cp command on fly as follows:

_message="Usage: chkfile commandname"
# Run another command (compact format)
_cmd="${2:? $_message $(cp $_file $HOME/.output)}"
$_cmd "$_file"

3. Find Variable Length

You can easily find string length using the following syntax:

echo "${#variableName}"
# print it #
echo "$len"

Here is a sample shell script to add a ftp user:

# Usage : Add a ftp user
# die if username/password not provided
[ $# -ne 2 ] && { echo "Usage: addftpuser username password"; exit 1;}
# Get username length and make sure it always <= 8
[[ ${#_fuser} -ge 9 ]] && { echo "Error: Username should be maximum 8 characters in length. "; exit 2;}
# Check for existing user in /etc/passwd
/usr/bin/getent passwd "${_fuser}" &>/dev/null
# Check exit status
[ $? -eq 0 ] && { echo "Error: FTP username \"${_fuser}\" exists."; exit 3; }
# Add user
/sbin/useradd -s /sbin/nologin -m  "${_fuser}"
echo "${_fpass}" | /usr/bin/passwd "${_fuser}" --stdin

Each Linux or UNIX command returns a status when it terminates normally or abnormally. You can use command exit status in the shell script to display an error message or take some sort of action. In above example, if getent command is successful, it returns a code which tells the shell script to display an error message. 0 exit status means the command was successful without any errors. $? holds the return value set by the previously executed command.

4. Remove Pattern (Front of $VAR)

The syntax is as follows:


You can strip $var as per given pattern from front of $var. In this example remove /etc/ part and get a filename only, enter:

echo "${f#/etc/}"

We see the file name:


The first syntax removes shortest part of pattern and the second syntax removes the longest part of the pattern. Consider the following example:


You just want to get filename i.e. dnstop-20090128.tar.gz, enter (try to remove shortest part of $_url) :

echo "${_url#*/}"

Sample outputs:


Now try using the longest part of the pattern syntax:

echo "${_url##*/}"

Sample outputs:


This is also useful to get a script name without using /bin/basename:

echo "$_self is called"

Create a script called as follows:

# Purpose: Display jail info as per softlink
# Author: Vivek Gite
# find out script name
[ "$VERBOSE" == "1" ] && echo "Called as $_self for \"$_j\" domain(s)"
for j in $_j
	export _DOMAIN_NAME=$j
        # call appropriate functions as per script-name / softlink
	case $_self in echo "Upload dir for $j: $(get_domain_upload_dir)" ;; echo "/tmp dir for $j: $(get_domain_tmp_dir)" ;;
       echo "$j domain mem usage (php+lighttpd): $(get_domain_mem_info)" ;;
       echo "$j domain cpu usage (php+lighttpd): $(get_domain_cpu_info)" ;;
       echo "$j domain user and group info: $(get_domain_users_info)" ;;
       echo "$j domain disk quota info (mysql+disk): $(get_domain_diskquota_info)" ;;
		*) warn "Usage: $_self"

Finally, create softlink as follows:
# ln -s
# ln -s
# ln -s

You can now call script as follows:
# ./
# ./

#4.1: Remove Pattern (Back of $VAR)

The syntax is as follows:


Exactly the same as above, except that it applies to the back of $var. In this example remove .tar.gz from $FILE, enter:

echo "${FILE%.tar.gz}"

Here is what I get:


Rename all *.perl files to *.pl using bash for loop as Apache web server is configured to only use .pl file and not .perl file names:

for p in /scripts/projects/.devl/perl/*.perl
	mv "$p" "${p%.perl}.pl"

You can combine all of them as follows to create a build scripts:

# Usage: Build suhosin module for RHEL based servers
# Author: Vivek Gite
# ----
# Set default value for $2
# Get tar ball names
# Remove .tgz and get dir name
# Download software
wget "$URL" -O "${DLHOME}/$FILE"
wget "$vURL" -O "${DLHOME}/$vFILE"
# Extract it
tar -zxvf "$FILE"
cd "$DEST"
# Build it and install it
phpize --clean && phpize && ./configure && make && read -rp "Update/Install $SOFTWARE [Y/n] ? " answer
shopt -s nocasematch
[[ $answer =~ y|es  ]] && make install
shopt -u nocasematch

If you turn on nocasematch option, shell matches patterns in a case-insensitive fashion when performing matching while executing case or [[ conditional expression.

5. Find And Replace

The syntax is as follows:


Find word unix and replace with linux, enter:

x="Use unix or die"
sed 's/unix/linux/' <<<$x

You can avoid using sed as follows:

echo "${x/unix/linux}"
echo "${out}"

To replace all matches of pattern, enter :


You can use this to rename or remove files on fly

cp "${y}" "${y/.conf/.conf.bak}"

Here is another example:

         # RHEL php modules path
	for i in $_php_modules/*
		p="${i##*/}"                  ## Get module name
		ini="/etc/php.d/${p/so/ini}"  ## Get ini file by replacing .so with .ini extension
                # make sure file exists
		[ ! -f "$ini" ] && echo "$i php module exists but $ini file not found."

The following function installs required modules in chrooted php-cgi process

        # get jail name
	local n="${_chrootbase}/${d##/}"
	local p=""
	local ini=""
        # enable only ${_php_modules_enabled} php modules and delete other .ini files if exists in jail
	for i in $_php_modules/*
                # find out if module is enabled or not
		if [[ ${_php_modules_enabled} = *${p}*   ]]
			[ "$VERBOSE" == "1" ] && echo " [+] Enabling php module $p"
			$_cp -f "$i" "$n/${_php_modules##/}"      ## install it
			copy_shared_libs "$i"                     ## get shared libs in jail too
			[ -f "${ini}" ] && $_rm -f "${ini}"	  ## if old .ini exists in jail, just delete it

6. Substring Starting Character

The syntax is as follows:


Expands to up to length characters of parameter starting at the character specified by offset.

#### strip extra slash from $file  ####

Extract craft word only:

echo ${x:3:5}"

To extract phone number, enter:

# strip std code
echo "${phone:4}"

7. Get list of matching variable names

Want to get the names of variables whose names begin with prefix? Try:

echo "${!VECH*}"

8. Convert to upper to lower case or vice versa

Use the following syntax to convert lowercase characters to uppercase:

# Turn vivek to Vivek (only first character to uppercase)
echo "${name^}" 
# Turn vivek to VIVEK (uppercase)
echo "${name^^}" 
echo "Hi, $USERNAME"
echo "Hi, ${USERNAME^}"
echo "Hi, ${USERNAME^^}"
# Convert everything to lowercase 
echo "Actual path: ${dest,,}"
# Convert only first character to lowercase 
echo "${src,}"

Only convert first character in $dest if it is a capital ‘H’:

echo "${dest,H}"
echo "${dest,H}"

See “Shell Scripting: Convert Uppercase to Lowercase” for more info.

Summary: String Manipulation and Expanding Variables

For your ready references here are all your handy bash parameter substitution operators. Try them all; enhance your scripting skills like a pro:

${parameter:-defaultValue} Get default shell variables value
${parameter:=defaultValue} Set default shell variables value
${parameter:?"Error Message"} Display an error message if parameter is not set
${#var} Find the length of the string
${var%pattern} Remove from shortest rear (end) pattern
${var%%pattern} Remove from longest rear (end) pattern
${var:num1:num2} Substring
${var#pattern} Remove from shortest front pattern
${var##pattern} Remove from longest front pattern
${var/pattern/string} Find and replace (only replace first occurrence)
${var//pattern/string} Find and replace all occurrences
${!prefix*} Expands to the names of variables whose names begin with prefix.
Convert first character to lowercase.
Convert all characters to lowercase.
Convert first character to uppercase.
Convert all character to uppercase..


🐧 Get the latest tutorials on Linux, Open Source & DevOps via RSS feed or Weekly email newsletter.

🐧 33 comments so far... add one

CategoryList of Unix and Linux commands
Disk space analyzersdf duf ncdu pydf
File Managementcat cp mkdir tree
FirewallAlpine Awall CentOS 8 OpenSUSE RHEL 8 Ubuntu 16.04 Ubuntu 18.04 Ubuntu 20.04
Modern utilitiesbat exa
Network UtilitiesNetHogs dig host ip nmap
OpenVPNCentOS 7 CentOS 8 Debian 10 Debian 8/9 Ubuntu 18.04 Ubuntu 20.04
Package Managerapk apt
Processes Managementbg chroot cron disown fg glances gtop jobs killall kill pidof pstree pwdx time vtop
Searchingag grep whereis which
User Informationgroups id lastcomm last lid/libuser-lid logname members users whoami who w
WireGuard VPNAlpine CentOS 8 Debian 10 Firewall Ubuntu 20.04
33 comments… add one
  • Jeffrey Oct 14, 2010 @ 3:23

    I hate to go offtopic, but do you know if these apply to tcsh as well?


    • 🐧 nixCraft Oct 14, 2010 @ 16:54

      I’ve not used tcsh / csh much; but should work with ksh.

    • Chris F.A. Johnson Oct 6, 2011 @ 2:04

      These are POSIX shell expansions and bash/ksh93 extensions. They do not apply to tcsh (which should not be used for scripting).

  • Lacrymology Oct 14, 2010 @ 16:22

    I hate to go offtopic as well, but I love the Internet Explorer 9 ad on the right….. wtf?

    • 🐧 nixCraft Oct 14, 2010 @ 16:54

      Ads comes from various sources. This is the only way to cover the cost of hosting, CDN and bandwidth. Hope this helps!

  • TMW Oct 14, 2010 @ 16:36

    So you got post back but deleted all comments? How come?

    • 🐧 nixCraft Oct 14, 2010 @ 16:39

      Backups are done ones a day. This post was restored from my local open office org cache file. There is not much I can do to restore all 12-13 comments including yours.

  • jackd Oct 15, 2010 @ 2:07

    What a great tutorial and reference. I can never remember the syntax when I need it. Will definitely be bookmarking this one.

  • Lucas Vieites Oct 15, 2010 @ 7:29

    Great tips! There are some functionalities I didn’t even know existed.

  • thiagoc Oct 18, 2010 @ 2:11

    cp “${y}” “${y/.conf/.conf.bak}”

    a more simple approach:

    cp “${y}”{,.bak}

    • Chris F.A. Johnson Oct 6, 2011 @ 2:07

      Or, more portably and efficiently:

      cp "$y" "$y.bak"
      • maedox Dec 6, 2011 @ 11:01

        cp ${y,.bak}

        • Chris F.A. Johnson Nov 8, 2013 @ 14:36

          In bash, a comma in parameter expansion converts to lower case:

          $ y=HELLO
          $ echo "${y,}"
          $ echo "${y,,}"
          • maedox Nov 8, 2013 @ 14:48

            You’re right. Not sure where my brain was that day. One would I assume I tested it before commenting, but it doesn’t seem to do anything. No error or warning either. That’s bash for you. 😛

            • Chris F.A. Johnson Nov 8, 2013 @ 15:19

              There’s no reason for any error message or warning; it’s a perfectly legal, if nonsensical, expansion.

  • Matt Thiessen Oct 22, 2010 @ 12:47

    Man, did I need this today. Thanks!

  • gamerh2o Dec 4, 2010 @ 8:12

    very useful,like this site very much

  • toto Feb 16, 2011 @ 5:59

    good tutorial,


  • unixgeek Feb 23, 2013 @ 1:52

    Thanks for this excellent HowTo! One minor correction: sed ‘s/unix/linux’ <<<$x should be sed 's/unix/linux/g' <<<$x

    Otherwise you get an unterminated `s' command error.

  • Tom Jun 24, 2013 @ 18:30

    on the bash-shell-parameter-substitution-2.html page, _mkdir() example, fourth line you write:
    [ $# -eq 0 ] &shouldn’t this be
    [ $# -eq 0 ] &## [0—] is four digit octal permissions

    or equivalent to allow user to set permissions other than the default 0755 value?

  • Tom Jun 24, 2013 @ 18:35

    prior message was strangely truncated in places. Resubmit:

    on the bash-shell-parameter-substitution-2.html page, _mkdir() example, fourth line you write:

    [ $# -eq 0 ] &shouldn’t this be:

    [ $# -eq 0 ] &## [0—] is four digit octal permissions

    to inform user to set permissions other than the default 0755 value if desired?

  • Tom Jun 24, 2013 @ 18:38

    Last attempt: on the bash-shell-parameter-substitution-2.html page, _mkdir() example, 4th line:
    [ $# -eq 0 ] &shouldn’t this be:
    [ $# -eq 0 ] &## [0—] is octal permissions
    to inform user to set other than the default 0755 value if desired?

  • srinivasu Nov 8, 2013 @ 5:25

    Nice! learnt something from this tutorial..

  • Zigmund Nov 12, 2013 @ 21:55

    Something interesting to note on the substring syntax is that you can address the offset backwards as well. I am not sure about how portable this syntax is, but I have found it very useful:

    echo “${VAR: -4}” # Prints the last 4 characters, note the space between : and – in this example.

    echo “${VAR:1:-1}” # The space is optional on the second offset, but not the first

  • khoan Sep 20, 2014 @ 15:45

    -bash: value: command not found

    • maedox Sep 25, 2014 @ 11:07

      @khoan, yes, what did you expect? There is no command named ‘value’. Put echo in front and it should output ‘value’ unless $bam is already set.

  • wd40 Jan 25, 2015 @ 19:44

    Just what i wanted to learn. Thanks!

  • Ikem Feb 3, 2015 @ 16:50

    How much does that apply to “dash”?

  • Chris F.A. Johnson Feb 7, 2015 @ 19:30

    Ikem, all the POSIX expansions (#1..#4) will work in dash (but not the shopt command).

  • Alhadis Feb 13, 2015 @ 14:12

    … aaaaand BOOKMARKED. *punches Command+D* Thanks for the helpful reference! 😀

  • Meetali Mandar Deshmukh Nov 26, 2020 @ 1:05

    Superb post! Bookmarked. This is the type of content I was looking for while learning bash scripting.

Leave a Reply

Your email address will not be published.

Use HTML <pre>...</pre> for code samples. Still have questions? Post it on our forum