---
title: Make your own shell command
description: Define a shell function in a file, source it from your shell config, and call it like any built in command. Why chmod is not part of it and which names to avoid.
tags: [shell, bash, zsh, terminal, productivity]
type: blog
date: 2026-08-30
---

A shell function loaded at startup behaves exactly like `ls` or `cd`: you type
its name and it runs. There are three steps. Write the function in a file,
`source` that file so the current shell knows about it, and add the source line
to your shell config so every future shell knows too.

## Write the function

Put it in a dotfile in your home directory. The name does not matter, only that
you can remember it.

```bash
touch ~/.commands.sh
```

```bash
# ~/.commands.sh
greet() {
  echo "hello world"
}
```

`greet() { ... }` and `function greet { ... }` both work in bash and zsh. The
first form is POSIX and works in more shells, so prefer it.

## Load it

```bash
source ~/.commands.sh
```

`source` reads the file in the shell you are already in, so everything it
defines is still there when it finishes. `.` is the same command with a shorter
name: `. ~/.commands.sh`.

This is also why `chmod +x` is not part of the process. The execute bit lets the
kernel run a file as a program, which starts a new shell to do it. That child
shell would define the function, run to the end, and exit, taking the function
with it. Sourcing avoids the child entirely, and a file you only ever source
never needs to be executable.

## Make it permanent

Sourcing affects one shell. Close the terminal and the function is gone. To get
it in every new shell, put the source line in your shell's startup file.

```bash
echo 'source ~/.commands.sh' >> ~/.zshrc    # zsh
echo 'source ~/.commands.sh' >> ~/.bashrc   # bash
```

macOS has used zsh as the default since Catalina, so `~/.zshrc` is usually the
right one. Check with `echo $SHELL` if you are not sure.

## Reload after an edit

New shells read the file on startup, so they are already current. The shell you
edited in is not, and it keeps the old definition until you tell it otherwise.

```bash
source ~/.commands.sh
```

Run that after every change. Forgetting it is the usual reason an edit seems to
have done nothing.

## Do not shadow an existing command

Your function wins over anything already on the system with that name, which is
worth being careful about. `test` is a real POSIX builtin that scripts use for
conditionals, so a function called `test` quietly breaks other people's scripts
and some of your own shell's behaviour.

Check the name before you commit to it.

```bash
type greet
command -v greet
```

If either prints a path or a description, the name is taken. Silence or "not
found" means it is free.

## Function or alias

An alias substitutes text at the start of a command and nothing else. A function
can take arguments, branch, and run several commands, so anything with logic
wants to be a function.

```bash
alias ll='ls -lah'

mkcd() {
  mkdir -p "$1" && cd "$1"
}
```

Quote `"$1"` every time. Unquoted, a path with a space in it arrives as two
separate arguments and the command does the wrong thing.

## When a script is the better answer

A function only exists inside shells that sourced it. Nothing else can see it:
not cron, not another program, not a script running under a different shell.

When you need any of those, make it a real file on your `PATH` instead. This is
where `chmod +x` belongs, because now the file genuinely is being executed.

```bash
mv mytool ~/bin/mytool
chmod +x ~/bin/mytool
export PATH="$HOME/bin:$PATH"    # add this line to ~/.zshrc as well
```

Start the file with a shebang, `#!/usr/bin/env bash`, so the system knows what
to run it with.
