Tech Handbook Null Yard

Shell Scripting - Debian and FreeBSD

Related topics: Debian Practical Shell Handbook, FreeBSD Practical Shell Handbook, Shells in Debian and Shells in FreeBSD.

1. Which shell should you choose for scripts?

For portable system scripts use POSIX sh. Use Bash only when you intentionally need Bash-specific syntax. FreeBSD /bin/sh and Debian /bin/sh are not Bash.

2. First script

#!/bin/sh

echo "Hello"

Make it executable with chmod +x script.sh and run ./script.sh.

3. echo and printf - printing information

echo "Starting..."
printf 'Status: %s\n' "$status"

Prefer printf when exact formatting matters.

4. Variables

name="user"
echo "$name"

Do not put spaces around =. Quote variable expansions unless you explicitly want word splitting/globbing.

5. Environment variables

export APP_ENV=production
printf '%s\n' "$APP_ENV"

6. Reading user input

printf 'Name: '
read -r name
printf 'Hello %s\n' "$name"

7. Yes / no questions

printf 'Continue? [y/N] '
read -r answer
case "$answer" in
  y|Y|yes|YES) echo "Continuing" ;;
  *) echo "Cancelled" ;;
esac

8. if statement

if command; then
  echo "success"
else
  echo "failure"
fi

9. test and [ ]

if [ -f "$file" ]; then
  echo "file exists"
fi

Spaces inside [ ... ] are mandatory because [ is a command.

10. Most important tests

Does a file exist?

[ -f "$file" ]

Does a directory exist?

[ -d "$dir" ]

Does something exist regardless of type?

[ -e "$path" ]

Is the file executable?

[ -x "$file" ]

Is the file writable?

[ -w "$file" ]

Is the variable empty?

[ -z "$value" ]

Is the variable non-empty?

[ -n "$value" ]

11. Comparing numbers

[ "$a" -eq "$b" ]
[ "$a" -lt "$b" ]
[ "$a" -gt "$b" ]

12. case - ideal for menus

case "$choice" in
  1) do_one ;;
  2) do_two ;;
  q|Q) exit 0 ;;
  *) echo "Unknown option" ;;
esac

13. Reacting to command result

some_command
status=$?
echo "exit status: $status"

14. Best way to check command success

if some_command; then
  echo "OK"
else
  echo "FAILED" >&2
fi

Usually test the command directly rather than checking $? later.

15. && and ||

build && deploy
command || echo "failed" >&2

16. Output redirection

command >output.txt
command >>output.txt
command 2>errors.txt
command >all.txt 2>&1

17. Status messages

printf '[INFO] %s\n' "Starting backup"
printf '[ERROR] %s\n' "Backup failed" >&2

18. Functions

log() {
  printf '[INFO] %s\n' "$*"
}

log "Starting"

19. Arguments passed to the script

echo "$0"
echo "$1"
echo "$2"
echo "$#"
echo "$@"

20. Checking required arguments

if [ "$#" -lt 1 ]; then
  echo "Usage: $0 FILE" >&2
  exit 2
fi

21. exit - ending a script

Use exit 0 for success and non-zero values for failure. Pick consistent exit codes for meaningful error classes.

22. Negation !

if ! command -v git >/dev/null 2>&1; then
  echo "git not found" >&2
fi

23. for loops

for file in *.log; do
  echo "$file"
done

24. while loop

while read -r line; do
  echo "$line"
done < file.txt

25. Menu loop

while :; do
  printf '1) status  q) quit\n> '
  read -r choice
  case "$choice" in
    1) status_cmd ;;
    q|Q) break ;;
  esac
done

26. Detecting the operating system

os=$(uname -s)
case "$os" in
  Linux) echo "Linux" ;;
  FreeBSD) echo "FreeBSD" ;;
  *) echo "Unsupported: $os" >&2; exit 1 ;;
esac

27. Debian + FreeBSD script

Branch only where platform commands differ, e.g. apt/systemctl on Debian versus pkg/service/sysrc on FreeBSD.

28. Command substitution

hostname=$(hostname)
now=$(date '+%F %T')

29. Arithmetic

count=$((count + 1))
total=$((a + b))

30. Default variable value

port=${PORT:-8080}
name=${1:-default}

31. Checking whether a program exists

if command -v curl >/dev/null 2>&1; then
  echo "curl available"
fi

32. Safer scripts

Quote variables, validate inputs, stop on real errors deliberately, use temporary files safely and keep destructive operations explicit.

33. Comments

# Explain why, not the obvious syntax.

Check Internet connectivity

if ping -c 1 1.1.1.1 >/dev/null 2>&1; then
  echo "network reachable"
fi

34. Clear script structure

Configuration

Constants/defaults and configurable paths belong near the top.

Functions

Put reusable actions into small named functions.

Validation

Check required commands, arguments, files and privileges before changing anything.

Main program

Keep the actual execution flow short and readable.

...

For larger scripts, split logic into separate files or move to a more suitable language.

35. stderr - error messages

echo "error" >&2

36. Running a script without ./

Put it in a directory listed in PATH, such as ~/.local/bin, and make it executable.

37. Best place for your own user scripts

~/.local/bin

38. Adding ~/.local/bin to PATH

export PATH="$HOME/.local/bin:$PATH"

39. Where to configure PATH

Bash

Usually ~/.profile for login environment and ~/.bashrc for interactive Bash-specific setup.

POSIX sh

Use a login/profile file appropriate to the shell/session, commonly ~/.profile.

Zsh

Use ~/.zshenv/~/.zprofile/~/.zshrc according to whether the setting is environment, login or interactive.

tcsh / csh

Use the csh/tcsh configuration files and setenv PATH ... syntax.

40. Reloading changes without logging out

. ~/.profile

For Bash interactive config use source ~/.bashrc or . ~/.bashrc.

41. Checking where a script runs from

command -v mytool
which mytool

42. Scripts available to all users

System-wide custom commands commonly belong in /usr/local/bin; install them as root with appropriate ownership/mode.

43. Script filename

Use simple lowercase names without spaces; a .sh suffix is optional for installed command-like tools.

44. Aliases vs scripts

Aliases are interactive shortcuts; scripts are reusable programs that work from any shell/session with predictable behavior.

45. Practical example - checking a server

#!/bin/sh
set -u
echo "Host: $(hostname)"
uptime
df -h
if command -v systemctl >/dev/null 2>&1; then
  systemctl --failed
fi

46. Practical example - administration menu

Use a while loop plus case to offer status, logs, restart and quit actions.

47. Practical example - confirmation before operation

printf 'Delete %s? [y/N] ' "$target"
read -r answer
case "$answer" in y|Y) rm -- "$target" ;; *) exit 0 ;; esac

48. Practical example - system update script

Detect OS, then run Debian apt update/upgrade or FreeBSD pkg update/upgrade, logging failures and exit status.

49. Practical example - series of commands with statuses

Wrap each operation in a function that prints [OK] or [FAIL] and returns a meaningful status.

50. Practical example - tool accepting commands

Use case "$1" to implement subcommands such as status, start, stop, logs.

51. More elaborate example

A useful admin tool can combine config variables, validation, OS detection, logging functions, subcommands and cleanup traps.

52. return vs exit

return leaves a function or sourced script. exit terminates the whole shell script process.

53. Pipeline

ps aux | grep nginx
find . -type f | sort | uniq

54. Searching text

grep -Rni 'error' /var/log
grep -E 'error|warning' file.log

55. Handling Ctrl+C and trap

cleanup() { rm -f "$tmp"; }
trap cleanup EXIT INT TERM

56. Temporary files

tmp=$(mktemp) || exit 1
trap 'rm -f "$tmp"' EXIT

57. Debugging a script

sh -x script.sh

Use set -x only where useful because traces may expose secrets.

58. Syntax checking

sh -n script.sh
bash -n script.sh

59. ShellCheck

shellcheck script.sh

ShellCheck catches quoting, portability and logic issues.

60. shellcheck + Vim

Run ShellCheck from Vim/Neovim through a command, quickfix integration or linting plugin, but keep the CLI command usable independently.

61. Permissions

chmod 755 ~/.local/bin/mytool
ls -l ~/.local/bin/mytool

62. Script requiring root

if [ "$(id -u)" -ne 0 ]; then
  echo "Run as root" >&2
  exit 1
fi

Prefer narrow sudo rules or specific privileged commands where possible.

63. Script configuration

Keep non-secret defaults in a config file or environment variables; do not hard-code credentials.

64. User configuration directory

~/.config/mytool/config

65. Colors - optional

Use colors only for interactive terminals and provide plain output for logs/pipes. Check [ -t 1 ] before ANSI styling.

66. Basic template for your own tool

#!/bin/sh
set -u

die() { echo "ERROR: $*" >&2; exit 1; }

main() {
  command -v curl >/dev/null 2>&1 || die "curl required"
  # work
}

main "$@"

67. Example installation of your script

mkdir -p ~/.local/bin
install -m 755 mytool ~/.local/bin/mytool
command -v mytool

68. When the script should be really “installed”

Use /usr/local/bin for the executable, /usr/local/etc or /etc for system config, documentation/man page if appropriate, and package it if many hosts/users need managed upgrades.

69. What to learn next

Quoting/word splitting, getopts, traps, robust temp files, text processing (awk, sed), and when to switch from shell to Python/Go.

70. getopts - first step toward a proper CLI

while getopts 'vf:' opt; do
  case "$opt" in
    v) verbose=1 ;;
    f) file=$OPTARG ;;
    *) exit 2 ;;
  esac
done

71. Most important rules to remember

Quote variables

rm -- "$file"

Check errors

Do not assume a command succeeded; test it or let the failure propagate deliberately.

Use functions

Functions keep repeated actions and error handling readable.

Keep your own programs in the right place

Use ~/.local/bin for user tools and /usr/local/bin for system-wide local tools.

Use command -v

command -v curl >/dev/null 2>&1 || exit 1

Check the script

sh -n script.sh
shellcheck script.sh

72. Minimum knowledge needed to write useful scripts

Shebang, variables, quoting, input, if/test, case, loops, functions, arguments, exit codes, redirection, command substitution, PATH and error handling.

73. How to think about a shell script

A shell script is glue around existing commands. Use it to orchestrate tools; do not force complex data structures/business logic into shell when another language is clearer.

74. Small project to build yourself

Create a serverctl tool with status, logs, restart, backup and update subcommands, supporting Debian and FreeBSD.

Cheat sheet

Script start

#!/bin/sh

Message

printf '%s\n' "hello"

Variable

name=value
echo "$name"

Input

read -r value

Condition

if [ -f "$file" ]; then ...; fi
case "$choice" in ... esac

Function

fn() { ...; }

Command result

if command; then ...; fi

Argument

first=$1

Command result into variable

host=$(hostname)

Check a program

command -v git

Make available from every directory

install -m 755 tool ~/.local/bin/tool

Check

sh -n tool
shellcheck tool

Summary

Good shell scripts are small, explicit and defensive: quote input, validate assumptions, use exit codes, separate stderr, clean up temporary files and rely on existing Unix tools.

Official references

  • POSIX Shell Command Language: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html
  • Debian Reference - shell programming: https://www.debian.org/doc/manuals/debian-reference/ch12
  • FreeBSD sh(1): https://man.freebsd.org/cgi/man.cgi?query=sh&sektion=1