Skip to content

How to create shell scripts in macOS

Headshot of Andrea Pepper, SimpleMDM writer and MacAdmin
Andrea Pepper|January 27, 2025
generalnewbanner
generalnewbanner

Shell scripting in macOS is an excellent way to tackle multiple tasks, streamline workflows, and manage devices effectively. Whether it’s your first time or you're a seasoned IT professional, creating shell scripts for Mac can save time and effort by consolidating repetitive tasks.

What is a shell script? 

A shell script is a text file (.txt) containing one or more UNIX commands that execute sequentially in a shell environment. Instead of entering commands manually into the Terminal application, you can automate tasks by writing them into a script.

Common shell types: macOS supports multiple shells, including Z shell (zsh), Bash (bash), and others like /bin/sh, for flexibility and compatibility:

  • /bin/zsh: The default shell in macOS since Catalina (10.15). 

  • /bin/bash: The previous default shell in macOS, still widely used for scripting. 

  • /bin/sh: A lightweight, POSIX-compliant shell for portability and legacy scripts. 

Benefits of using shell scripts

Shell scripts simplify complex tasks, reduce manual errors, and save time. 

Examples include: 

  • Automating backups. 

  • Managing system configurations. 

  • Performing batch file processing. 

  • Setting up development environments. 

While shell scripts may seem intimidating, taking the time to learn them will help make your mobile device management pretty damn quick.

SimpleMDM Favicon

Streamline your Apple device management

Try SimpleMDM free for 30 days to see how the ultimate Apple MDM helps you manage your fleet with ease.

Key components of a shell script

A script can have many components, but you’re likely to run into the following:

  1. Shebang (#!): The first line of every script specifies the interpreter (the shell) to execute the script. For example: #!/bin/zsh
    This line ensures the script runs in the specified shell (e.g., zsh or bash). 

  2. Commands: The actual instructions, such as file operations, text processing, system calls, or running other programs. e.g.: cd

  3. Variables: Used to store and reuse data, e.g., name="John" or count=10

  4. Control structures: Logic for decision-making and repetition, including conditionals and loops.

    • conditionals (if, case)

    • loops (for, while, until)

  5. Comments: Lines starting with # are used to document and explain the script’s functionality but do not affect script actions.

  6. Functions: Reusable blocks of code defined and called within the script.

  7. Input/Output redirection: Directs input and output, e.g.: 

    • > for writing to files 

    • < for reading input 

    • and | for piping 

  8. Arguments and options: Parameters passed to the script to customize behavior ($1, $2, etc.). 

  9. Error handling: Includes constructs, like set -e or checking exit statuses ($?), to manage script reliability.

  10. External commands and utilities: Calls to system utilities or programs for specific tasks, such as:

    • ls

    • grep

    • awk

Importance of the shebang

The first line of the script is always the shebang. The shebang consists of two symbols: #!

  • The # indicates the start of a comment in most shell scripts.

  • The ! immediately following the # signals that this line is unique — it specifies the path to the interpreter executing the script.

For example:
#!/bin/zsh

This line tells the system to use the Z shell (zsh) as the script's interpreter. Without the shebang, the script might run in the user’s default shell, which can lead to compatibility issues if the script is designed for a different shell. 

Include the shebang (#!) at the beginning of the script to specify which interpreter you’d like to use for your script. 

For macOS:

  • Use #!/bin/zsh for Z shell (default shell). 

  • Use #!/bin/bash for bash. 

Dynamic shebang for portability

To enhance portability, you can dynamically locate the interpreter’s path using: 

#!/usr/bin/env zsh 

Using /usr/bin/env makes the shebang more portable, as it doesn’t rely on a fixed path for the interpreter (like /bin/zsh or /usr/bin/zsh).

Instead, it works in environments where the location of zsh might differ or where users have custom paths set up for their shells.

  • The env command dynamically locates the interpreter (in this case, zsh) by searching the system’s $PATH environment variable. 

  • This ensures that the script uses the first instance of zsh found in the user’s environment, regardless of its exact installation path.

Common shell commands

Here are some frequently used commands that you’ll use to customize your scripts depending on the desired actions.

Command 

Description 

cat 

Read and display the content of one or more files. 

cd 

Change the directory you’re working in. 

chmod 

Modify file or folder permissions. 

chown 

Modify file or folder ownership. 

cp 

Copy files. 

curl 

Send or receive data from a web server — think sending API requests to your MDM. 

defaults 

Read software preference files. 

echo 

Return the result of a command to standard output. 

grep 

Search a file for a given pattern. 

killall 

Stop a running process or app. 

ls 

List the contents of a directory. 

man 

View the manual page for a command. 

mkdir 

Create a new directory or folder. 

mv 

Move one or more files or directories. 

/usr/libexec/PlistBuddy 

Modify software preference files. 

pwd 

See the directory you’re currently working in. 

rm 

Delete files. 

rmdir 

Delete a directory. 

Note: Rm and rmdir are both delete actions; please use caution. 

Write your first shell script

Something I didn’t quite understand when attempting to run my first script in the Terminal is that you can’t just type the script into the Terminal and expect it to execute immediately; you need to save some sort of text editor file first. 

I’ll go over how to use two macOS onboard options:

  • TextEdit: A simple text editor for macOS that supports both plain text and rich text formatting, often used for quick notes or script creation. 

  • nano: A simple, user-friendly command-line text editor for UNIX/Linux systems, designed for basic editing tasks directly in the Terminal.

Other recommended editors include Visual Studio Code and Sublime Text

Feel free to follow along with the provided simple example for the script:

1 2 #!/bin/zsh  echo "Hello, World!”

Using TextEdit

  1. Open the TextEdit application.

  2. Switch to plain text mode.

    • By default, TextEdit uses rich text. Switch to plain text by selecting Format > Make Plain Text or pressing Shift + Cmd + T

  3. Enter your script. Copy and paste the following example into the document:

    1 2 #!/bin/zsh  echo "Hello, World!”
  4. Save the file.

    • Select File > Save (Cmd + S). 

    • Name the file hello_world.zsh 

    • Ensure the format is set to UTF-8 and avoid appending the .txt extension to the filename.

Using nano

  1. Open the Terminal.

  2. Navigate to the desired directory (optional).

    • Use cd to navigate to the directory where you want to save the script:cd ~/Documents

  3. Create or edit the script.

    • Type the following command to create or edit a file named hello_world.zsh:
      nano hello_world.zsh
      After typing nano followed by your script, the following window should open in the Terminal. This is nano.

      The Nano window in the Terminal.

  4. Enter your script.

    • In nano, type:

      1 2 #!/bin/zsh  echo "Hello, World!”

  5. Save and exit. 

    • Save your changes by pressing Ctrl + O (the letter O, not the number zero), then press Enter to confirm the filename.

    • Exit nano with Ctrl + X.

      If you have unsaved changes, nano will prompt you to save before exiting. 

Instead of nano, you can also use VIN within the Terminal interface if you’d like more robust options. 

Additional nano tips 

  • Navigation: Use the arrow keys to move around the file.

  • Cut, Copy, and Paste: Use Ctrl + K to cut text, Ctrl + U to paste text, and Ctrl + ^ to mark text for cutting.

  • Help: Press Ctrl + G to access a list of commands in nano.

Choosing an extension

  • Use .zsh: For scripts utilizing Z shell-specific features or primarily for zsh environments. 

  • Use .sh: For scripts adhering to POSIX standards, designed to run across multiple shell types. 

  • The file extension is a helpful identifier, but the shebang (#!) defines which interpreter runs the script.

Make your script executable

By default, newly created scripts are not executable for security reasons. This ensures that malicious or unintended scripts are not accidentally run. To make your script executable, use the chmod command: 

Terminal window on macOS showing commands to make a script executable using chmod +x and run it, outputting "Hello, World!"

What is chmod?

The chmod command (short for change mode) adjusts file permissions, which control who can:

  • read 

  • write 

  • or execute (a file) 

One crucial step for every script is using chmod to make the script executable after it has been written. 

By default, a newly created script is treated as a regular text file and cannot be run as a program. The chmod command allows you to modify the file’s permissions, explicitly enabling the execute permission for the script. 

For example, to make a script executable, you can use:

chmod +x script_name.sh This command modifies the file’s permissions, allowing the script to be executed directly from the Terminal. Without chmod, you would need to invoke the interpreter to run the script explicitly.

Additional examples of chmod usage include: 

chmod u+x myscript.sh

  • Grants execute permission to the file’s owner. 

chmod g+x myscript.sh

  • Grants execute permission to the file’s group.

By marking a script as executable, chmod ensures your scripts are user-friendly and behave like stand-alone programs. It’s a small but essential step in preparing your scripts for use.

Add documentation

Comments are essential in shell scripting. They improve readability and make scripts easier to maintain. Create comments by placing a hash sign (#), aka octothorpe, at the beginning of the line. The shell ignores everything following the # on that line during execution. 

How to add comments to your script

Adding comments is pretty straightforward.

Use # to include single-line comments that explain your script’s purpose or individual commands.

For example: 

#!/bin/zsh  # This is a simple script to print a message echo "Hello, World!" # This line prints "Hello, World!" to the terminal 

#!/bin/zsh 

# This is a simple script to print a message

echo "Hello, World!" # This line prints "Hello, World!" to the Terminal

Types of comments

  1. Single-line comments
     Use a full line for comments to explain sections or overall functionality: # This script prints a greeting message

  2. Inline comments
    Add comments on the same line as a command to clarify its purpose: echo "Hello, World!" # This prints "Hello, World!"

Tips for writing comments

  • Explain purpose: Use comments to explain what a section of the script does, especially if it’s not immediately clear. 

  • Annotate complex logic: A comment can clarify your intent for any complex logic or nonobvious code. 

  • Avoid over-commenting: While it’s important to comment on your code, avoid adding comments that do not provide additional value beyond what the code already clearly states. 

Test your script 

Testing ensures your scripts run smoothly before deployment, so always test before deploying your scripts to a production environment.

  1. Run scripts in a safe environment: Test potentially destructive commands (e.g., rm) in a controlled environment, such as a test directory.

  2. Use logging to track execution: Log important actions for debugging:

    1 2 #!/bin/zsh echo "Script executed at $(date)" >> /tmp/script.logq
  3. Test across multiple shells: Test scripts in different shell environments to ensure compatibility: bash myscript.sh
    zsh myscript.sh

  4. Simple test scripts: Verify functionality with simple commands: 

    1 2 #!/bin/bash echo "Script ran at $(date)" >> /tmp/test-log.txt

    Example of a basic test script:

    1 2 #!/bin/zsh echo "Ran script at $(date)" >> /tmp/script-test.txt

    This appends the current date and time to /tmp/script-test.txt to verify the script runs successfully.

Troubleshoot and debug your script

Handling errors during execution

Use conditional statements to handle errors effectively:

1 2 3 4 5 #!/bin/zsh if [ ! -d "/path/to/directory" ]; then echo "Directory does not exist." exit 1 fi

Debugging techniques

Even your most beautiful, well-written scripts can encounter a few issues!

Use these debugging techniques to keep rolling:

  1. Run in debug mode: Add -x to the shebang or command: #!/bin/zsh -x This prints each command before executing it.

  2. Log outputs: Redirect errors to a log file.

  3. Check permissions: Ensure the script is executable by running: chmod +x myscript.sh If permissions are correct but the script still fails, try running it with sudosudo ./myscript.sh

Sudo allows users to run commands as the superuser (root) for performing administrative tasks with elevated privileges while ensuring security through password authentication and logging. 

To rerun the last command with admin permissions, type sudo !!

Syntax errors

Syntax errors often occur due to typos or incompatible commands. Use zsh -n to check for syntax errors without executing the script: zsh -n myscript.sh If there are no syntax errors, the command returns to the prompt without output. Otherwise, it indicates the syntax issue.

Debugging execution

To debug a script and trace its execution, use verbose mode: zsh -x myscript.sh This displays each command before execution, helping you identify where the script fails.

Tools and best practices

launchd

launchd is the system-wide service manager for macOS, responsible for starting, stopping, and managing daemons and agents. It controls tasks and services that run in the background using .plist files.

For example, you can use launchd to automate a daily backup task by scheduling a script that copies essential files to an external drive every night.

  • Use launchd to automate the execution of shell scripts at specified intervals or events, such as startup, login, or file changes. 

  • Use launchctl to load, unload, and manage services.

Daemons vs. agents

Aspect 

Daemons 

Agents 

Context 

System-wide 

User-specific 

Runs Before Login 

Yes 

No 

Interacts with UI 

No 

Yes 

Privileges 

System-level (root) 

User-level 

Shell script examples

  1. Cleaning temporary files
    This script deletes files in the /tmp directory older than 7 days: 

    1 2 3 #!/bin/zsh find /tmp -type f -mtime +7 -exec rm {} \; echo "Cleaned temporary files older than 7 days."

    A Terminal window on macOS showing the command zsh -n clean_temp_files.sh.

  2. System health check 
    This script checks disk usage and available memory, appending the results to a log file: 

    1 2 3 4 5 6 #!/bin/zsh echo "System Health Check - $(date)" >> /tmp/system_health.log echo "Disk Usage:" >> /tmp/system_health.log df -h >> /tmp/system_health.log echo "Memory Usage:" >> /tmp/system_health.log vm_stat >> /tmp/system_health.log
    A nano text editor open on macOS displaying a script named system_health_check.sh that logs system health information, including disk usage and memory usage, to a file in /tmp/system_health.log.

  3. Syncing directories
    This script synchronizes files between two directories using rsync: 

    1 2 3 #!/bin/zsh rsync -av --delete /path/to/source/ /path/to/destination/ echo "Directories synced successfully.”

Shell scripting resources

  • Manual pages: You can access detailed documentation for commands directly in the Terminal using the man command. Manual pages provide comprehensive information about commands, options, and usage examples.

    • How: Type man followed by the command name in the Terminal to open the relevant manual page.

  • Example: man chmod

  • Apple’s Shell Scripting Primer 


macOS shell scripts FAQ 

What are POSIX standards?

POSIX (Portable Operating System Interface) standards define a set of operating system interfaces, utilities, and shell scripting conventions to ensure compatibility between different UNIX-like systems. They provide guidelines for system APIs, command-line tools, and environment behavior to promote software portability.

What is verbose mode?

Verbose mode is a setting that provides detailed information about a program or script's behavior. It is often used for debugging or monitoring. Verbose mode typically outputs additional messages to the console to explain each execution step.

What is the difference between VIM and nano?

Both Vim and nano are used within the Terminal, but they cater to different audiences of varying technical expertise.

  • Vim: A powerful, configurable text editor with multiple modes, ideal for advanced users who need extensive features and customization. Great for programmers and sysadmins.

  • nano: A straightforward, easy-to-use text editor designed for simplicity and quick edits, making it ideal for beginners or those who prefer a simple interface. 

Headshot of Andrea Pepper, SimpleMDM writer and MacAdmin
Andrea Pepper

Andrea Pepper is an Apple SME MacAdmin with a problematic lack of impulse control around a software update prompt. When not poking at machines, Pepper enjoys being a silly goose in sunny Colorado with her two gigantic fluffer pups.

Related articles