hashub

hashub

Shell Basics and Advanced

1. Shell Script Syntax#

1.1 Conditional Testing: test [#

The command test or [ can test whether a condition is true. If the test result is true, the command's Exit Status is 0; if the test result is false, the command's Exit Status is 1 (note that this is exactly the opposite of the logical representation in C language).
For example, testing the size relationship between two numbers:

$ VAR=2
$ test $VAR -gt 1
$ echo $?
0
$ test $VAR -gt 3
$ echo $?
1
$ [ $VAR -gt 3 ]
$ echo $?
1

Although it seems strange, the left bracket [ is indeed the name of a command, and the parameters passed to the command should be separated by spaces, for example, $VAR, -gt, 3, ] are four parameters of the [ command, and they must be separated by spaces. The parameter forms of the test or [ command are the same, except that the test command does not require the ] parameter. Taking the [ command as an example, common test commands are shown in the table below:

Test Commands

CommandDescription
[ -d DIR ]True if DIR exists and is a directory
[ -f FILE ]True if FILE exists and is a regular file
[ -z STRING ]True if the length of STRING is zero
[ -n STRING ]True if the length of STRING is non-zero
[ STRING1 = STRING2 ]True if the two strings are identical
[ STRING1 != STRING2 ]True if the strings are not identical
[ ARG1 OP ARG2 ]ARG1 and ARG2 should be integers or variables with integer values, OP is one of -eq (equal), -ne (not equal), -lt (less than), -le (less than or equal), -gt (greater than), -ge (greater than or equal)

Test Commands with AND, OR, NOT

CommandDescription
[ ! EXPR ]EXPR can be any of the test conditions in the above table, ! indicates logical negation
[ EXPR1 -a EXPR2 ]EXPR1 and EXPR2 can be any of the test conditions in the above table, -a indicates logical AND
[ EXPR1 -o EXPR2 ]EXPR1 and EXPR2 can be any of the test conditions in the above table, -o indicates logical OR

For example:

$ VAR=abc
$ [ -d Desktop -a $VAR = 'abc' ]
$ echo $?
0

Note that if the $VAR variable in the above example is not defined beforehand, it will be expanded to an empty string by the Shell, causing a syntax error in the test condition (expanded to [ -d Desktop -a = 'abc' ]). As a good Shell programming practice, the value of variables should always be enclosed in double quotes (expanded to [ -d Desktop -a "" = 'abc' ]):

$ unset VAR
$ [ -d Desktop -a $VAR = 'abc' ]
bash: [: too many arguments
$ [ -d Desktop -a "$VAR" = 'abc' ]
$ echo $?
1

1.2 if/then/elif/else/fi#

Similar to C language, in Shell, branching control is implemented using the commands if, then, elif, else, and fi. This flow control statement is essentially composed of several Shell commands, for example, as previously mentioned:

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

This is actually three commands: if [ -f ~/.bashrc ] is the first command, then . ~/.bashrc is the second command, and fi is the third command. If two commands are written on the same line, they need to be separated by a ;, and if only one command is written on a line, there is no need to write the ;. Additionally, if there is a newline after then, but the command is not finished, the Shell will automatically continue the line, treating the next line as part of the command following then.
Like the [ command, it is important to note that there must be spaces between the command and each parameter. The parameters of the if command form a sub-command; if the Exit Status of that sub-command is 0 (indicating true), the sub-command following then is executed; if the Exit Status is non-zero (indicating false), the sub-commands following elif, else, or fi are executed. The sub-command following if is usually a test command, but it can also be other commands. Shell scripts do not have {} brackets, so fi indicates the end of the if statement block.
See the example below:

#! /bin/sh

if [ -f /bin/bash ]
then echo "/bin/bash is a file"
else echo "/bin/bash is NOT a file"
fi
if :; then echo "always true"; fi

: is a special command known as null command, which does nothing but always returns true for Exit Status. Additionally, you can also execute /bin/true or /bin/false to get true or false Exit Status.
Let's look at another example:

#! /bin/sh

echo "Is it morning? Please answer yes or no."
read YES_OR_NO
if [ "$YES_OR_NO" = "yes" ]; then
  echo "Good morning!"
elif [ "$YES_OR_NO" = "no" ]; then
  echo "Good afternoon!"
else
  echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
  exit 1
fi
exit 0

In the above example, the read command waits for the user to input a line of string and stores that string in a Shell variable.

In addition, Shell also provides && and || syntax, similar to C language, with Short-circuit characteristics. Many Shell scripts prefer to write like this:

test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)

&& is equivalent to “if...then...”, while || is equivalent to “if not...then...”. && and || are used to connect two commands, while the -a and -o mentioned above are only used to connect two test conditions in test expressions, so it is important to note the difference between them. For example,

test "$VAR" -gt 1 -a "$VAR" -lt 3

is equivalent to the following:

test "$VAR" -gt 1 && test "$VAR" -lt 3

1.3 case/esac#

The case command can be likened to the switch/case statement in C language, and esac indicates the end of the case statement block.
The case in C language can only match integer or character constant expressions, while the case in Shell scripts can match strings and wildcards. Each matching branch can have several commands, and must end with ;;. When executed, it finds the first matching branch and executes the corresponding commands, then directly jumps to after esac, without needing to use break to exit like in C language.

#! /bin/sh

echo "Is it morning? Please answer yes or no."
read YES_OR_NO
case "$YES_OR_NO" in
yes|y|Yes|YES)
  echo "Good Morning!";;
[nN]*)
  echo "Good Afternoon!";;
*)
  echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
  exit 1;;
esac
exit 0

Examples of using the case statement can be found in the script directory for system services /etc/init.d/. Most scripts in this directory have this form (taking /etc/apache2 as an example):

case $1 in
	start)
		...
	;;
	stop)
		...
	;;
	reload | force-reload)
		...
	;;
	restart)
	...
	*)
		log_success_msg "Usage: /etc/init.d/apache2 {start|stop|restart|reload|force-reload|start-htcacheclean|stop-htcacheclean}"
		exit 1
	;;
esac

The command to start the apache2 service is:

$ sudo /etc/init.d/apache2 start

$1 is a special variable that automatically takes the value of the first command line argument when executing the script, which is start, so it enters the start) branch to execute the corresponding commands. Similarly, specifying the command line argument as stop, reload, or restart can enter other branches to execute commands related to stopping the service, reloading the configuration file, or restarting the service.

1.4 for/do/done#

The for loop structure in Shell scripts is quite different from that in C language; it resembles the foreach loop in some programming languages. For example:

#! /bin/sh

for FRUIT in apple banana pear; do
  echo "I like $FRUIT"
done

FRUIT is a loop variable; in the first iteration, the value of $FRUIT is apple, in the second iteration it is banana, and in the third iteration it is pear. For example, to rename files chap0, chap1, chap2, etc. in the current directory to chap0~, chap1~, chap2~, etc. (by convention, filenames ending with ~ indicate temporary files), this command can be written as:

$ for FILENAME in chap?; do mv $FILENAME $FILENAME~; done

It can also be written like this:

$ for FILENAME in `ls chap?`; do mv $FILENAME $FILENAME~; done

1.5 while/do/done#

The usage of while is similar to that in C language. For example, a password verification script:

#! /bin/sh

echo "Enter password:"
read TRY
while [ "$TRY" != "secret" ]; do
  echo "Sorry, try again"
  read TRY
done

The following example controls the number of loop iterations through arithmetic operations:

#! /bin/sh

COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
  echo "Here we go again"
  COUNTER=$(($COUNTER+1))
done

Shell also has an until loop, similar to the do...while loop in C language. This chapter will skip that.

Exercises

  1. Modify the above password verification program so that if the user enters the wrong password five times, it reports an error and exits.

1.6 Positional Parameters and Special Variables#

There are many special variables that are automatically assigned by the Shell. We have already encountered $? and $1, and now let's summarize:

Common Positional Parameters and Special Variables

CommandDescription
$0Equivalent to argv[0] in the C language main function
$1, $2...These are called positional parameters, equivalent to argv[1], argv[2]... in the C language main function
$#Equivalent to argc - 1 in the C language main function; note that the # here does not indicate a comment
$@Represents the parameter list "$1" "$2" ..., for example, it can be used after in in a for loop.
$?The Exit Status of the previous command
$$The process ID of the current Shell

Positional parameters can be shifted using the shift command. For example, shift 3 means that the original $4 now becomes $1, the original $5 now becomes $2, and so on; the original $1, $2, $3 are discarded, and $0 does not move. The shift command without parameters is equivalent to shift 1. For example:

#! /bin/sh

echo "The program $0 is now running"
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"
shift
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"

1.7 Functions#

Similar to C language, Shell also has the concept of functions, but there are no return values or parameter lists in function definitions. For example:

#! /bin/sh

foo(){ echo "Function foo is called";}
echo "-=start=-"
foo
echo "-=end=-"

Note that there must be a space or newline between the left curly brace { and the following command in the function body. If the last command and the right curly brace } are written on the same line, a ; must be placed at the end of the command.

When defining the foo() function, the commands in the function body are not executed, just like defining a variable; it simply gives a definition to the name foo. The commands in the function body are executed only when calling the foo function (note that function calls in Shell do not use parentheses). Functions in Shell scripts must be defined before they are called, and generally, function definitions are written at the beginning of the script, while function calls and other commands are written at the end of the script (similar to the main function in C language, which is where the actual execution of commands in the entire script begins).

The absence of a parameter list in Shell functions does not mean that parameters cannot be passed. In fact, functions are like mini-scripts; when calling a function, any number of parameters can be passed, and within the function, the parameters can be extracted using $0, $1, $2, etc. The positional parameters in the function are equivalent to local variables of the function, and changing these variables does not affect the $0, $1, $2, etc. variables outside the function. The return command can be used in functions to return a value; if a number follows return, it indicates the Exit Status of the function.

The following script can create multiple directories at once, with the directory names passed as command line arguments. The script tests each directory to see if it exists; if a directory does not exist, it first prints a message and then tries to create that directory.

#! /bin/sh

is_directory()
{
  DIR_NAME=$1
  if [ ! -d $DIR_NAME ]; then
    return 1
  else
    return 0
  fi
}

for DIR in "$@"; do
  if is_directory "$DIR"
  then :
  else
    echo "$DIR doesn't exist. Creating it now..."
    mkdir $DIR > /dev/null 2>&1
    if [ $? -ne 0 ]; then
      echo "Cannot create directory $DIR"
      exit 1
    fi
  fi
done

Note that is_directory() returns 0 for true and 1 for false.

2. Debugging Methods for Shell Scripts#

Shell provides several options for debugging scripts, as follows:
-n Reads through the commands in the script but does not execute them, used to check for syntax errors in the script.

-v Executes the script while printing the executed script commands to standard error output.

-x Provides tracing execution information, printing each executed command and its result sequentially.

There are three ways to use these options: first, by providing parameters on the command line:

$ sh -x ./script.sh

Second, by providing parameters at the beginning of the script:

#! /bin/sh -x

The third method is to enable or disable parameters in the script using the set command:

#! /bin/sh
if [ -z "$1" ]; then
  set -x
  echo "ERROR: Insufficient Args."
  exit 1
  set +x
fi

set -x and set +x indicate enabling and disabling the -x parameter, respectively, allowing for tracing debugging of only a specific section of the script.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.