Bash - Command Substitution, Subshell and Codeblock

Bash Linux

Bash - Command Substitution, Subshell and Codeblock

Quick and simple differences between using $(..), (..) and {..;}.

  • $(command) or `command` - Command substitution (the output of a command replaces the command name) and is executed in a subshell. Please give preference to $(..) for better readability
  • ( command ) - Command list is executed in a subshell
  • { command ;} - Command list is executed in the current shell

Executing code

$ ( echo 123 )
123

$ $(echo 123) # this executes the output of the command
123: command not found

$ { echo 123 ;}
123

Fiding subshell level with built-in BASH_SUBSHELL variable

$ ( echo $BASH_SUBSHELL )
1
$ ( ( echo $BASH_SUBSHELL ) )
2

$ $($BASH_SUBSHELL)
1: command not found

$ { echo $BASH_SUBSHELL ;}
0

Passing variables to subshell

$ var1=1

$ ( echo $var1 )
1

$ $(echo $var1)
1: command not found

$ { echo $var1 ;}
1

Changing and getting variables values in subshell

$ var=1

$ ( var=2 ) ; echo $var
1

$ $(var=2) ; echo $var
1

# Because we did not create a subshell,
# variable is available to original shell
$ { var=2 ;} ; echo $var
2

Other usages

Example 1:

Multiple commands and multiple lines. This can be used with (..) and {..}.

$ { echo 1 ; echo 2 ; echo 3; }
1
2
3

$ {
> echo 1
> echo 2
> echo 3;
> }
1
2
3

Example 2:

Changing the environment for subshell with separate environment. Here, set -u is only set in the subshell, so only the first echo will error out.

unset var1 var2
(
  set -u
  echo $var2
)
echo $var1

Redirecting Bash Xtrace to a Separate Log

Linux Bash

So you have a Bash script that you want to troubleshoot, but you want to send stdout to a file, and stderr to another. Here’s a solution.

For example, I like to use Bash’s color to display failure or success on checks, and echo’s removal of new lines (echo -e "Wait for it...\c ") to wait for checks. For example, the screenshot below shows a script that check each step and report back.

terminal

Errors are displayed in red, and success in green, and this same output is sent to the stdout and to a log file as well.

Now I want to get error messages in a separate log file, so I can go back to my original log and match the time where it failed, with additional verbose from Bash’s xtrace.

The solution is very simple… I can just add the lines below to the top of my script:

# Debugging
debug_log="${HOME}/bin/log/script.debug"
exec 2>>${debug_log}
set -x

The first line setups my debug log, the second redirects stderr to my debug log, and the third enables xtrace (same as bash -x [script]).

Note that doing this will disable any errors from being show in stdout.

How to Record and Share a Terminal Session

Linux Shell

A while ago I had shared instructions on “How to Share a Terminal Window Online”. This is great for live support.

Today I’m sharing instructions on how to record your teminal session, and not only share it, but also allow users to copy and paste text from the playback video.

The installation could not be easier. And to show how great this utility is, I’m showing the instructions using a “video” recorderd with “showterm”.

Now go ahead and try copying text from the video. Isn’t that great!?


Reference:

http://showterm.io/

https://github.com/ConradIrwin/showterm

How to Create a Prompt With Timeout in Bash

Linux Bash

Here’s a quick function that will display a prompt with timeout in a bash script:

_myCountdownFunction () {
echo -e "Hit \"Ctrl+c\" to quit or \"Enter\" to continue...    \c"
cnt=5
while (( cnt >= 0 )) ; do
  if (( cnt < 9 )) ; then
    echo -e "\b\b${cnt}s\c"
  elif (( cnt == 9 )) ; then
    echo -e "\b\b\b ${cnt}s\c"
  elif (( cnt <= 99 )) ; then
    echo -e "\b\b\b\b ${cnt}s\c"
  elif (( cnt < 999 )) ; then
    echo -e "\b\b\b\b${cnt}s\c"
  fi
  read -t 1 my_reply
  (( $? == 0 )) && exit 1
  let cnt-=1
done
}

The user will see the following message on his terminal with the seconds counting down in place:

Hit "Ctrl+c" to quit or "Enter" to continue... 5s

At the end of the specified time in ‘cnt’, the script (where the function would be) will continue executing, and if the user hits “Ctrl+c” before that, it will exit (both script and function).

The function supports up to 999s (which should be enough).

Bash Scripting - Best Practices

Linux Bash

Bash Scripting - Best Practices

1 - Readability

1.1 - Indentation

There are 3 commonly used indentation practices for Bash (I prefer the first method, however all 3 are “accepted”):

  • 2 spaces
  • 4 spaces
  • tabs (usually 8 spaces)

All examples will be shown using the first indentation method, however for reference here’s a comparison between all 3.

Example:

## 2 spaces
if ...
  command
else ...
  command
fi

## 4 spaces
if ...
    command
else ...
    command
fi

## Tabs
if ...
        command
else ...
        command
fi

Indentation for if conditional statements

if [ test ] ; then
  command
elif [ test ] ; then
  command
else
  command
fi

Indentation for for statements

for a b c in $A ; do
  command
done

Indentation for while and until loops

while [ true ] ; do
  command
done

Indentation for case statements

case $VAR in
  true) command ;;
  false)
    command1
    command2
    ;;
  *)
    if [ test ] ; then
      command
    fi
    ;;
esac

Indentation for functions

_funct_do_var() {
  commands
}

1.2 - Comments

Code Comments

You should always comment your code. This will make it easier for others to understand, as well as for yourself should you need to change the script a few months (or years) later.

Make sure that the comments make sense. Do not try to save on typing as additional comments can save you (or someone else) a lot of time in the future.

Example: Commonly used comments

startTMATE() {
  ## Starts tmate handling
  # Launch tmate in a detached state
  $TMATE_BIN -S $TMT_SOCKET new-session -d

  # Blocks until the SSH connection is established
  $TMATE_BIN -S $TMT_SOCKET wait tmate-ready

  # Prints the SSH connection string
  $TMATE_BIN -S $TMT_SOCKET display -p '#{tmate_ssh}' > $LOG

  # Prints the read-only SSH connection string
  $TMATE_BIN -S $TMT_SOCKET display -p '#{tmate_ssh_ro}' >> $LOG
}

Example: In-line comments

## Sets up package manager aliases based on distro
# Settings for Ubuntu
if [[ "$DISTRO" =~ (Ubuntu|LinuxMint) ]] ; then
  alias aptdate='sudo apt-get update'         # Updates package list
  alias aptgrade='sudo apt-get upgrade'       # Updates all packages
  alias apts='apt-cache search'               # Search for package
  alias aptrm='sudo apt-get remove'           # Removes package

  aptinst() {                                # Install package
    if [ -d "${HOME}/bin/var/log" ] ; then
      LOG=${HOME}/bin/var/log/$(hostname)-package-install.log
    fi

    echo -e "$(date)\t-\tInstalling packages: $*" >> "$LOG"
    sudo apt-get install "$*"
  }
fi

Block Comment

Here Document

You can use a here document (EOF) to “trick” Bash in creating block comments.

Example:

alt text

You can read a bit on my other post here.

Bash Built-in Command

You can also use Bash’s bultin :, which does nothing.

Here’s the definition from Bash’s man page:

: [arguments]
    No effect; the command does nothing beyond expanding arguments and
    performing any specified redirections. A zero exit code is returned.

Example:

: '
 your comments here
'

1.3 - Lines

Breaking Line of Code

When breaking long lines of code with \, indent the new lines.

Example:

[ -f "/etc/cups/cupsd.conf" ] && echo \
  "The CUPS configuration file exists"

Using Empty Lines

You can use empty lines to keep your code clean, even inside code blocks.

Example:

# Downloads this weeks photos
if [ "$THIS_WEEKS_PHOTOS_URL" ] ; then
  cd ${UNSPLASH_DIR}
  for PHOTO_URL in $THIS_WEEKS_PHOTOS_URL ; do

    # Checks if jpg is in the URL
    if [[ $(echo $PHOTO_URL | grep -qi jpg ; echo $?) -eq 0 ]] ; then
      PHOTO_FILE_NAME=$(echo $PHOTO_URL | awk -F/ '{print $4}')

    # else, let's add it
    else
      PHOTO_FILE_NAME=$(echo $PHOTO_URL | awk -F/ '{print $4}').jpg
    fi

    # Get the photo and save as file name
    wget --progress=bar $PHOTO_URL -O $PHOTO_FILE_NAME
  done
fi

1.4 - Misc

  • Avoid the use of back-ticks ‘`’ for command substitution. Use $(...) for better readability

2 - Headers and Sections

2.1 - Script Header

Add descriptive headers to the script outlining it’s name, what it does, usage and possibly version.

#!/bin/bash

################################################################################
################################################################################
# Name:          tmate.sh
# Usage:
# Description:   Runs tmate and outputs remote string to file
# Created:       2014-10-31
# Last Modified:
# Copyright 2014, Victor Mendonca - http://wazem.org
# License: Released under the terms of the GNU GPL license
################################################################################
################################################################################

2.2 - Sections

Divide your script into sections.

Put all global script variables in one section

#-------------------------------------------------------------------------------
# Sets variables
#-------------------------------------------------------------------------------

Put all functions in one section

#-------------------------------------------------------------------------------
# Functions
#-------------------------------------------------------------------------------

Put all major work in one section (where functions get called)

#-------------------------------------------------------------------------------
# Starts script
#-------------------------------------------------------------------------------

3 - Variables

3.1 - Naming

  • Give meaningful names to variables
  • When using uppercase, make sure the variable is not already being used
  • Avoid starting variables with _

3.2 - Calling

Double quote variables to prevent globbing or word splitting.

4 - Functions

Try to define your functions at the beginning of the script (see sections).

4.1 - Naming

  • Should start with lower case
  • It’s good to start with _
  • Upper and lower case are also good

Example:

_lowerIt () {
  BASH_MAIN_VERSION=$(echo $BASH_VERSION | awk -F. '{print $1}')
  if [[ $BASH_MAIN_VERSION -lt 4 ]] ; then
    echo "Not supported with your version of bash"
    return
  fi

  if [ "$1" ] ; then
    echo ${1,,}
  fi
}

4.2 - Local Variables

Keep your function variables local if they are not being used outside of the function.

Example: This script will output “$2400” only once

_myFunction() {
  local extract
  extract='$2400'
  echo "$extract"
}

_myFunction
echo "$extract"

5- Code Smart

New and Deprecated Features

Get to know your version of Bash and any features it may have.

Example: Old and new arithmetic expansion

# New way for arithmetic expansion
$ echo $((4*25/2))
50

# Old way for arithmetic expansion, which will be deprecated
$ echo $[4*25/2]
50

New features

Bash 4 packs new features (like parameter expansion and output redirects). Get familiar with them as they can save you a lot of time.

Examples: Parameter expansion

$ title="bash best practices"

## Case substitution
$ echo ${title^^}
BASH BEST PRACTICES

## String substitution
$ echo ${title/best/worst}
bash worst practices

Examples: stdout and stderr output redirect

# New
ls 123 &> /dev/null

# Old
ls 123 > /dev/null 2>&1

6 - Software

You can also use software to help with your scripts.

6.1 - Syntax Highlighting

Most of today’s editors include syntax highlighting, even VIM. Syntax highlighting will help you catch mistakes like a unclosed bracket or quote.

6.2 - Lint

Some editors, like Sublime Text include plugins for lint software. They can be of great help for you coding.

6.3 - Online Tools

There are also sites that can help checking your script (similar to lint), or a small set of commands.

References

code with