COMP(2041|9044) Shell Style Guide

Contents

Shell Language & Shell Files Recommended Code Layout Variables Functions Commands Static Verification & Correctness

Shell Language & Shell Files

Language Features

Shell scripts must be written in POSIX compliant shell syntax.

The POSIX definition of the "Shell Command Language" can be found in IEEE Std 1003.1-2017.

for simplicity, while not entirely true, all shell features available in the dash(1) shell are considered to be POSIX compliant.

Therefor all shell scripts must be able to execute using the dash(1) shell.

Shell scripts must not contain shell feature from bash(1), zsh(1), or other shells.

Language Features

The goal if this course is to teach you how to write shell scripts that are portable.

This means that we want your shell scripts to be able to run on as many platforms as possible with minimum to no modification.

In order to achieve this we want to make sure that your shell scripts are written in the shared subset other shell languages.

This is what POSIX provides.

Other shell languages like bash(1) and zsh(1) implement extensions on top of POSIX.

Writing shell scripts that do not rely on these extensions allows the scripts to be used in both bash(1) and zsh(1) as well as other shells.

File Names

Unless otherwise stated, the names of shell scripts must end with a .sh file extension.

File Names

Some activities might ask for a different (or no) file extension.

If this is not explicitly stated, the file extension must be .sh.

File Names

    hello.sh
    program.sh
    isPrime.sh
    
File Names

    hello.shell
    program.dash
    isPrime
    
Hashbang

Shell scripts must start with a Hashbang line.

Hashbang

Sometimes also called a shebang line, the hashbang line is used to specify the shell interpreter to use.

A Hashbang line is a line that starts with a #! and contains the path to the shell interpreter.

As we are using the dash(1) shell, we need to specify the path to the dash(1) interpreter.

The Hashbang can directly specify the path to the dash(1) interpreter
Or it can specify the path to the env(1) utility which will then run the dash(1) interpreter.

Using the env(1) utility is recommended.

Specifying sh(1) as the interpreter might run dash(1) or bash(1), depending on the platform, and thus is not allowed.

The Hashbang characters #! must be the first two characters in the file.
The Hashbang is not valid if there is whitespace before the #! characters.

The Hashbang is valid if there is whitespace after the #! characters and before the path.
This is recommended as it is easier to read the path.

Hashbang

#! /usr/bin/env dash	<- recommended

#!/bin/dash         	<- also acceptable
Hashbang

#!/usr/bin/env bash	<- not allowed, as it specifies the bash interpreter

#! /bin/sh         	<- not allowed, as it is ambiguous what shell will be used
Permissions

Shell scripts must have read and execute permissions enabled for all users.

Permissions

In order to execute a shell script with ./file.sh, the file must have read and execute permissions enabled.

To enable read and execute permissions, use the chmod command:

        chmod a+rx file.sh
    

Or:

        chmod 755 file.sh
    

Without the execute permission, the file can still be executed, with:

        dash file.sh
    

But the read permission is still required.

Permissions

    $ ls -l isPrime.sh
    -rwxr-xr-x 1 cs2041 cs2041 2.2K May 30 02:37 isPrime.sh	<- correct permissions
    
Permissions

    $ ls -l isEven.sh isOdd.sh
    -rw-r--r-- 1 cs2041 cs2041 2.2K May 30 02:37 isEven.sh 	<- missing execute permission
    -rwxr-x--- 1 cs2041 cs2041 2.2K May 30 02:37 isOdd.sh	<- missing other permissions
    

Recommended Code Layout

Header Comment

All programs must start with a header comment.

Header Comment

The header comment must be at the top of the file,
immediately after the hashbang line,
with a single empty line between the hashbang line and the header comment.

There must be a single empty line between the header comment and the first line of your program.

The header comment should contain at least:

  • The name of the activity
  • The name of the file
  • The name, zID, and email address of the author.
  • The date it was written.
  • A description of what the program does.

The preferred style for a header comment is:

        # ACTIVITY-NAME-HERE
        # FILE-NAME-HERE
        #
        # This program was written by YOUR-NAME-HERE (YOUR-ZID-HERE) <YOUR-EMAIL-HERE>
        # on DATE-HERE
        #
        # DESCRIPTION-HERE
    

For programs that are being updated/maintained over a long period of time, the header comment should also contain a changelog/updatelog.

        # ACTIVITY-NAME-HERE
        # FILE-NAME-HERE
        #
        # DESCRIPTION-HERE
        #
        # DATE-HERE
        # YOUR-NAME-HERE (YOUR-ZID-HERE) <YOUR-EMAIL-HERE>
        # - CHANGE-HERE
        #
        # DATE-HERE
        # YOUR-NAME-HERE (YOUR-ZID-HERE) <YOUR-EMAIL-HERE>
        # - CHANGE-HERE
    
Header Comment

        #! /usr/bin/env dash

        # COMP2041/9044 Lab06 - A Shell Script that Prints Itself
        # quine.sh
        #
        # This program was written by Dylan Brotherston (z5115658) <d.brotherston@unsw.edu.au>
        # on May 30, 2022
        #
        # This program is a quine.
        # A program prints its own source code (minus comments).

        b=\' c=\\ a='echo b=$c$b c=$c$c a=$b$a$b; echo $a'
        echo b=$c$b c=$c$c a=$b$a$b; echo $a
    

        #! /usr/bin/env dash

        # COMP2041/9044 Assignment 1 - My Big Shell Project
        # meaning_of_life.sh
        #
        # This program calculates and prints the meaning of life.
        #
        # 2024-02-05
        # Dylan Brotherston (z5115658) <d.brotherston@unsw.edu.au>
        # - Update the meaning of life to 42
        #
        # 2023-09-17
        # Dylan Brotherston (z5115658) <d.brotherston@unsw.edu.au>
        # - Initial version

        echo 42
    
Function Comments

Any function that is not both obvious and short must be commented.

Function Comments

Function Comments

    
Implementation Comments

All programs must have comments.

Implementation Comments

Where your code may be unclear to a reader, or require further explanation,
you should leave brief comments describing the purpose of the code you have written.

Make sure that Comments are descriptive of why your code is doing what it does.
And not just stating what the code does.

Comments should be on the line before the code they are describing - rather than on the same line
This helps to improve readability.

Comments should be indented to the same level as the code they are describing.

Very small or simple programs might not need comments.

But most programs should have some commenting comments.

Implementation Comments

    #! /usr/bin/env dash

    # COMP2041/9044 Lab06 - Hello World Again
    # hello.sh
    #
    # This program was written by Dylan Brotherston (z5115658) 
    # on May 30, 2022
    #
    # This program simply says hello.

    # for each name in the command line
    for name in $@; do
        # Use grep to match a name that looks like a zID
        if grep -E 'z[0-9]{7}'; then
            # If the name is a zID, then use `acc` to find the corresponding name
            # This allows us to say hello with the person's full name even if they only give use a zID
            # This means that even on a CSE server `./hello.sh $(whoami)` will work
            echo "Hello, $(acc format='$NAME')"
        else
            # If the name is not a zID, then just say hello to the name
            echo "Hello, $name!"
        fi
    done
    
Command Line Arguments

When using a command with options, you should use long options whenever possible.

Command Line Arguments

Short options are great when using the command line interactivly, but not when writing shell scripts.

Short options are not descriptive, and when using uncommon commands and/or options can be confusing to readers.

Long options are self descriptive, and make it easier to understand what the program is doing.

Not all commands support long options.
If you are using a command that does not support long options, you should use short options.

When using short options, you should use the combining syntax whenever possible

When using short options, you should add extra comments to explain what the option does.

Command Line Arguments

    jq --compact-output --slurp
    # non-standard command so use long options instead of the equivalent `-c -s`

    grep -E
    # common command, and common option, so using the short options is fine

    cut -d":" -f"1,2" --only-delimited
    # `--only-delimited` can be written as `-s` but is uncommon so use long option

    mutt -nRy
    # `mutt` does not support long options, so use short options instead
    # use the combining syntax to avoid writing `-n -R -y`
    
Controle Flow - Keywords

The keywords used to define a controle flow block should be on the same line as controle flow keyword

Controle Flow - Keywords

The keywords then, and do, should be on the same line as if, while, until, and for.

The code structure of ; then and ; do act the same as an open braces in C.

The code structure of fi, elif and done act the same as an close braces in C

The code structure of else, acts as both an open braces and close braces in C.

The alternative syntax with then and do on their own line
is allowed but not recommended

fi, elif, done, and else must be inedented to the same level as the if, while, until, or for.

If the alternative syntax is used then then, and do must be inedented to the same level as the if, while, until, or for.

Otherwise, they should be separated from the condition by a single semicolon and a single space.

Controle Flow - Keywords

    if [ $a -gt $b ]; then
      echo "a is greater than b"
    elif [ $a -lt $b ]; then
      echo "a is less than b"
    else
      echo "a is equal to b"
    fi

    while [ $a -lt $b ]; do
      isPrime $a
      a=$((a-1))
    done

    until [ $a -ge $b ]; do
      isPrime $a
      a=$((a-1))
    done

    for file in $files; do
      rm "$files"
    done
    
Controle Flow - Keywords

    # OK, but not recommended
    if [ $a -gt $b ]
    then
      echo "a is greater than b"
    elif [ $a -lt $b ]
    then
      echo "a is less than b"
    else
      echo "a is equal to b"
    fi

    # Too many spaces after the semicolon
    while [ $a -lt $b ];       do
      isPrime $a
      a=$((a-1))
    done

    # `do` and `done` should be indented in line with `until`
    until [ $a -ge $b ]
      do
      isPrime $a
      a=$((a-1))
      done

    # Too many empty lines before the `do`
    for file in $files

    do
      rm "$files"
    done
    
Controle Flow - Indentation

The body of controle flow statements must be inedented.

Controle Flow - Indentation

while loops, until loops, for loops, if statments, must be inedented.

Shell doesn't use braces for controle flow like C.

Instead, shell uses matching keywords to indicate the start and end of controle flow.

Everything between the start and end keyword must be inedented.

Between any pair of keywords, the indentation level should increase by one level.

It is recommended that you use 2 spaces as one level of indentation.

Other acceptable indentation levels are: 4 spaces or 1 tab

Whatever indentation level you use, it should always be consistent throughout your code.

Alaways indent be one level, and alawys use the same number of spaces or tabs to represent one level.

Never use a mixture of spaces and tabs.

Controle Flow - Indentation

    while [ "$i" -lt 10 ]; do
      echo "$i"
      i=$((i+1))
      n=$((n+i))
    done

    for arg in $@; do
      echo "$arg"
    done

    if [ ! -e "$file_name" ]; then
      echo "File does not exist"
    fi
    
Controle Flow - Indentation

    # inconsistent indentation
    while [ "$i" -lt 10 ]; do
        echo "$i"
      i=$((i+1))
            n=$((n+i))
    done

    # no indentation
    for arg in $@; do
    echo "$arg"
    done

    # way too much indentation
    if [ ! -e "$file_name" ]; then
                   echo "File does not exist"
    fi

    # mixing tabs and spaces
    if [ "$1" = "$2" ]; then
        echo "arg[1] and arg[2] are equal"
    	echo "that's a little redundant don't you think?"
    fi
    
Controle Flow - case

The body and alternatives of case statements must be inedented.

Controle Flow - case

case statements are used to select a single case from a list of cases.

Controle Flow - case

    case $command in
      login)
        token="$(login_user "$user" "$password")"
        if [ -z "$token" ]; then
          echo "Login failed"
          exit 1
        else
          echo "Login successful"
        fi
        ;;
      logout)
        logout_user "$token"
        ;;
      stats)
        cat /proc/self/stat
        cat /proc/self/status
        cat /proc/self/sched
        cat /proc/self/schedstat
        cat /proc/self/limits
        cat /proc/self/io
        ;;
      cmd)
        cat /proc/self/cmdline
        cat /proc/self/environ
        ;;
      *)
        echo "Unknown command: $command" >&2
        exit 1
        ;;
    esac

    case $level in
      0)     echo "Tutorial level" ;;
      [1-9]) echo "Level $level"   ;;
      10)    echo "Last level"     ;;
      *)
        echo "Unknown level: $level" >&2
        exit 1
        ;;
    esac
    
Spaces

Use a space after keywords such as:

if, while, for, return

Use a space on each side of binary operators such as

=  +  -  <  >  *  /  %   <=  >=  ==  !=

Do not use space after prefix unary operators such as

& * !

Do not use space before postfix unary operators such as

++ --
Do not use space on each side of member access operators such as
. ->
Spaces


    // This is correct:
    if (a + b > c) {
    i++;
    curr = curr->next;
    }


    // This is *not* correct:
    if(a+b>c) {
    i ++;
    curr = curr -> next;
    }
    
Vertical Whitespace

Use vertical whitespace (blank lines between code) occasionally to indicate where the related parts of your code are.

  • Using no blank lines between lines of code indicates the lines should be understood together, or are closely related.
  • Using exactly one blank line indicates two sections of code are distinct -- they split your code into "paragraphs".
  • Using exactly two blank line indicates two sections of code are entirely unrelated.
  • Using more than two blank lines is (almost) never appropriate.

The exact use of vertical whitespace is subjective, and your tutor will be able to give you feedback on your style throughout the term. Vertical whitespace is like chocolate -- some of it significantly improves your life, but too much can also be it's own problem; and used in the wrong place it can be very confusing.

Vertical Whitespace


    // The two variables here are closely related, and should not be seperated.
    int x_position = 0;
    int y_position = 0;


    // The two if statements here are seperate ideas, and could be seperated.
    if (x_position > 0) {
    printf("X position is positive.")
    }

    if (instruction == INCREASE_X_POSITION) {
    x_position += 1;
    }


    // We have used two empty lines here to indicate the the topic has changed.
    prinf("Please choose an item to use...\n")

    
Statements

IO redirection

Pipelines

Line Width

Whenever possible keep lines under 80 characters.

Line Width

Break long lines up to keep them under 80 characters,
unless keeping it as a longer line is significantly more readable.

At 120 characters, serious effort should be made to split or shorten the line.

Variables

Functions

Commands

Static Verification & Correctness