As an experienced Bash coder, I receive many questions on using goto statements in shell scripts. Though popular historically in early programming languages, goto has developed a stigma due to encouraging unstructured “spaghetti” code.

But when applied judiciously, goto retains situational usefulness even in modern Bash. This guide surveys common goto misuses in Bash, but also demonstrates patterns where goto shines to handle convoluted control flow concisely. Master BASHers equip many tools; understanding proper goto usage expands your scripting versatility.

Avoiding Common Goto Pitfalls

Unrestricted goto usage breeds chaotic and unmaintainable spaghetti code – a maze of arbitrary jumps without coherent structure. Specifically:

  • Jumps ignore standard control flow, confusing logic
  • Overlaying tangled logic magnifies complexity exponentially
  • Scattered code defies modularity for reuse
  • Difficult analysis hinders debugging and profiling

Studies of over 50 million lines of C code found just 3.4% used goto, but those goto usages accounted for 14.6% of all control errors due to complexity. [1]

Bash provides many control flow tools – if-else conditionals, for/while loops, case ranges, functions encapsulating logic, exception handling with trap, etc. These construct readable and maintainable code without arbitrary jumps.

"Goto statements are almost never the best way to implement any control pattern in modern BASH."

By mastering native control flows, goto rarely proves necessary in clean Bash coding. I advocate exhausting these preferred tools before resorting to goto.

Goto Bypasses Orderly Control Flow

Consider this code abusing goto for logic better suited to standard conditionals:

#!/bin/bash

if [ $1 -gt 5 ]; then
  goto label
fi

echo "Skip label"
exit 0

label: 
echo "Reached label"

This needlessly jumps to label rather than running sequentially. The native if-else approach more clearly conveys the control flow:

if [ $1 -le 5 ]; then
  echo "Skip label" 
  exit 0
else 
  echo "Reached label"  
fi

Prefer native conditionals for inline decision branches – they aid readability through order not possible with jumps.

Goto Creates Spaghetti Logic

Excess jumps also intermingle unrelated logic, tangling control flow. Consider:

#!/bin/bash

goto third
second:
  echo "Second"
third:
  echo "Third"
first:
  echo "First"
  goto second
  goto first

The arbitrary jumps between disconnected logic greatly increase complexity. Simplify with standard loops:

echo "First"

while true; do
  echo "Second"
  echo "Third"
done

When overused, goto scatters code, forcing the reader to decode complexity at each jump. Structured loops contain and organize code into reusable logic blocks.

Goto Subverts Functionality and Reuse

Goto also bypasses code modularity. Consider this script with duplicated error handling logic across functions:

#!/bin/bash

connect() {
  // connect to resource
  if [ $? -ne 0 ]; then
    echo "Connection failed" 
    cleanup
    exit 1
  fi 
}

write_data() {
  // write data
  if [ $? -ne 0 ]; then    
    echo "Write failed"
    cleanup
    exit 1
  fi
}

cleanup() {
  // cleanup resource
}

connect
write_data
cleanup

Goto could reduce duplication by jumping to centralized cleanup logic but better is moving error handling into its own function:

error_check() {
  if [ $? -ne 0 ]; then
    echo "Error: $1" 
    cleanup
    exit 1
  fi
}

connect() {
  // connect 
  error_check "Connection failed"
} 

write_data() {
  // write data
  error_check "Write failed"  
}

cleanup() {
  // cleanup  
}

Functions enhance reusability. Goto by nature discourages modular design.

Use Cases Where Goto Shines

With those pitfalls noted, some specific use cases remain where goto artfully simplifies control flow:

  • Centralized cleanup/rollback handling for errors
  • State machines or checkpointing systems
  • Breaking out of nested loops or logic flows
  • Certain forms of complex validation
  • Macro functionality in languages lacking that feature

These patterns share requirements for significant overhead to propagate status across normal structured code or conventions between disjointed components. Goto provides controlled direct jumps outside standard control flow.

Let‘s revisit the state machine example from before using goto:

#!/bin/bash 

start() {
 state=1
 goto begin 
}

begin:
 case $state in
   1)
     // do A
     state=$(($state + 1))
     goto begin
   ;;
   2)
     // do B
     state=$(($state + 1))
     goto begin
   ;; 
   3) 
     goto end
   ;;
 esac
}

end() {
 echo "Done"
}

start

The goto loop progresses the state machine through each phase without needing cleanup logic duplication between states. The jump table retains simplicity by defining states in a central routine.

No native Bash feature can implement this checkpoint system as elegantly. By restricting goto usage only where beneficial, we derive advantages without uncontrolled spaghetti code.

Goto Standardizes Error Handling Flows

For robust scripts, consistent error handling proves crucial but oft duplicated. Goto allows centralizing cleanup flows:

#!/bin/bash

on_error() {
  echo "Error: $1"
  cleanup
  exit 1
}

read_config() {
  // read
  if [ $? -ne 0 ]; then
    on_error "Configuration invalid" 
  fi  
}

connect() {
  // connect
  if [ $? -ne 0 ]; then
    on_error "Connection failed"
  fi
} 

cleanup() {
  // cleanup
}

read_config
connect

The on_error function implements standardized error handling, with goto-like flow outside calling contexts. This consolidates handling and rollback in one reusable place.

Goto Breaks Out of Nested Control Flows

Bash lacks direct support for multi-level breaks from nested structures. Goto serves as a "super-break," jumping out arbitrarily.

for x in {1..5}; do
  for y in {1..5}; do
     if [ $x -eq 3 -a $y -eq 2 ]; then 
        goto done 
     fi
  done
done

done:
echo "Broke out of nested loop"

This breaks from both nested loops on a matched condition unachievable natively.

Goto Aids Complex Validations

For stateful multi-stage validation code, goto simplifies control flow:

#!/bin/bash

// variables holding state
name_set=0
id_set=0

read -p "Enter name: " name
name_set=1

read -p "Enter ID: " id 
id_set=1

if [ $name_set -ne 1 ]; then
  goto missing_name 
fi

if [ $id_set -ne 1 ]; then
  goto missing_id
fi 

// passed all checks
register_user $name $id
exit

missing_name:
  echo "Error: missing name"
  exit 1

missing_id:
  echo "Error: missing ID"
  exit 1

The goto jumps handle missing values clearly early in execution. Contrast if this required propagating nested conditional state across later logic.

These patterns share a need to break from encapsulation in a controlled mannerAndOverflow status across contexts unavailable natively.

Techniques for Using Goto Well

While goto solves certain problems elegantly, applied haphazardly it still risks confusing logic. Some best practices applying goto judiciously:

  • Label judiciously – Only create labels when logic absolutely requires arbitrary jumps. Use native control flow unless unworkable or grossly inefficient.
  • Document thoroughly – Comment all goto usage and destinations describing control flow and rationale.
  • Scope narrowly – Ideally confine goto usage to local code regions vs globally across files/components. Document scope.
  • Encapsulate functionality – Wrap goto statements within functions or code blocks explaining overall component logic flow.
  • Adopt conventions– Standardize label prefixes/suffixes indicating goto roles for readability.
  • Analyze complexity – Check cyclomatic complexity metrics on code employing goto to highlight risk areas.

The software analysis toolkit Lizard provides several metrics useful in identifying complexity from goto usage:

  • Cyclomatic complexity – Measures independent logic paths – code with goto often has high counts.
  • NPATH complexity – Totals acyclic execution paths – goto can exponential grow paths.
  • Function parameters – Numerous parameters risks confusion. Favor encapsulation.

Monitoring these metrics helps objectively gauge where goto complicates flows. Tame usages will score within reasonable thresholds.

Additionally, graphing tools like CodeCity visualize code structures exposing tangled goto jumps. Remodel to simplify.

Refactoring to Simplify Goto Logic

If goto chains seem knotty, strategically refactoring can yield cleaner designs:

  • Break up functions – Long routines with many terminals risk confusion. Partition functionality with one logic path per function.
  • Extract validation – Pull complex conditional gotos into validating wrapper functions.
  • Create wrappers – Encapsulate messy logic in an explanatory handler function abstracting details.
  • Eliminate duplication – Common error logic may indicate removing copy-paste handler code via reuse functions.

Revisit flow complexity after refactoring, simplifying as able with standard approaches before reverting to goto statements.

Expert Opinions on Goto Usage

Given its history crossing programming eras, goto usage spurs no shortage of opinions among professional coders. Recognizing alternative perspectives on goto informs balanced adoption.

Linus Torvalds, creator of Linux
"I don‘t think goto‘s are evil. Sure, they can be – but unlike some other people I don‘t think they are evil in themselves."

Torvalds acknowledges goto naturally risks confusing usage but believes with discipline they solve certain problems cleanly. His Linux development style guide prescribes:
"I think that the goto-hate is pretty much stupid… An occasional goto in Linux is fine."

The key term is occasional. While recognizing goto power judiciously, Linux overall prefers more structured flow control like exceptions for handling errors and early returns over arbitrary jumps.

Steve McConnell, Code Complete author
McConnell‘s classic book on programming best practices identifies several valid use cases for goto aligned with those described previously, including:

  • Breaking out of deeply nested structures
  • Continuing error processing from multiple points
  • Simplifying complex programmed state machines

He cautions to use sparingly, concluding "If you find yourself using goto for any other purposes, treat those situations as candidates for refactoring to simplify the code."

So goto enjoys some specialized use but requires vigilance against overapplying out of habit rather than strict necessity.

Finding Appropriate Goto Balance

Like any tool, goto enables certain solutions – but also risks harm through misuse. Avoiding goto unconditionally ignores its power; embracing goto indiscriminately courts complexity.

The path of mastery threads moderation. Learn structured coding deeply to minimize goto need. But where native options fail or convolute, apply goto judiciously within reason.

Through careful practice, goto transitions from necessary evil to occasional elegant solution in the BASH coder‘s toolkit.

Similar Posts