I have a collection of command line cheatsheets I refer to often, saved as text files in a directory named “notes.”

% cd notes
% ls
git     mm         shell     zsh
less    postgres   sublime

It’s a quick and simple way of storing and viewing notes, dependent only on a convenient directory location, builtin commands, and the universality of text files.

But after years of keeping notes this way, I’ve grown tired of typing “less ~/notes” so often. So, I decided to learn something about zsh shell functions.


I kept the goals simple for my first function:

Function requirements

  • When called without arguments, list the files in ~/notes.
  • When called with the name of a note as an argument, display the note.

This would require learning a few things:

Learning requirements

  • How do I create and call a function?
  • How are arguments accessed in a function?
  • How do I determine if an argument is empty or not?
  • How do I execute commands based on a test?

I started with internet searches, reading manual pages, and hands-on experimentation to answer my questions.

For a better understanding, I’ve relied heavily on Chapter 3: Dealing with basic shell syntax of A User’s Guide to ZSH, the source of many of the explanations and examples shown here.

This is what I’ve learned.

Environment: I’m using zsh 5.8 on macOS 12.3.1, but the methods shown here should work on other systems and most versions of zsh.

Creating a function

You can define a function on the command line:

fn() {
    print I am a function

The shell understands you’re entering a function and changes the prompt after the first line. After entering “}” on the last line, the prompt returns to normal.

Enter the function’s name to run it.

% fn
I am a function

The spacing and new lines in this example are optional. The same function can be written as concisely as:

fn(){print i am a function}

Either way, the shell will display the function’s definition in this form:

% which fn
fn () {
  print i am a function.

A function created in the shell only exists in the current shell. It won’t exist in a newly launched shell.

% fn
i am a function
% zsh -c 'fn'
zsh:1: command not found: fn

It also won’t exist when you close your current shell. So, feel free to experiment.

Handling arguments

The shell uses special variables, known as positional parameters, to pass arguments to functions and scripts.

This function prints the number of arguments passed to it and the arguments:

args() {
  print $# $*

The $# holds the number of arguments and $* is an array holding the arguments.

Try calling the function with different arguments.

% args
% args hi
1 hi
% args these are arguments
3 these are arguments
% args 'an argument'
1 an argument

For more information see POSITIONAL PARAMETERS and PARAMETERS SET BY THE SHELL in the zshparam man pages.

Setting arguments interactively

The set command sets the special parameter that gets passed as an argument to functions or scripts and is accessed by positional parameters.

% set a whole load of words
% print $#
% print $*
a whole load of words

It’s exactly as if you were in a function and had called the function with the arguments “a whole load of words”.

To set the shell’s arguments to none, use -- after the command.

% set --
% print $#

For more information see SHELL BUILTIN COMMANDS in the zshbuiltins man pages (or enter help set).

Watch out for options

The set command can also set options for the shell. While arguments don’t effect the shell’s behavior, options do.

% set -v
% print verbose
print verbose

(Use set +v to turn verbose mode back off.)

To prevent arguments from being interpreted as options, use the special option --, which signals there are no more options.

% set -- -v
% print -- $*

Notice the same method is used with print to avoid -v being interpreted as an option when passed as a parameter.

Another example involves the positional parameter $0, which holds the name of the script or function being called.

% args() { print $0: $* }
% args hi
args: hi

But from the shell, the value of $0 is “-zsh”.

% set -- hi
% print $0: $*
print: bad option: -h
% print -- $0

As you can see, using -- with set and print is a good habit to get into.

Testing things

This is a common shell syntax for testing things:

if [[ -n $* ]]; then
  print yes
  print no

A conditional expression is used with the [[ compound command to test attributes of files and to compare strings. In this case, the -n tests whether the length of a string is non-zero.

There are many tests you can perform. See CONDITIONAL EXPRESSIONS in the zshmisc manual pages for more information.

In the shell, new lines and semicolons mean the same thing and are interchangeable. So, the same test could be rewritten as:

if [[ -n $* ]]; then; print yes; else; print no; fi

The logical command connectors && and || provide an even shorter syntax for testing. The && executes the command that follows if the prior command succeeded, while || executes the command that follows if the prior command failed.

true  &&  print Previous command returned true
false  ||  print Previous command returned false

Using this form, our test can be rewritten as:

[[ -n $* ]] && print yes || print no

See 3.81.1: Logical command connectors in the User’s Guide.

With these methods, we can easily verify our test works in the shell before trying it as a function.

% set some arguments
% [[ -n $* ]] && print -- $* || print empty
some arguments
% set --
% [[ -n $* ]] && print -- $* || print empty

The function

Putting everything together, it’s time to write my first function.

With notes stored at ~/notes, when this function is called with the name of a note, the note is displayed in less. Called without arguments, all the notes are listed.

note() {
  if [[ -n $* ]]; then
    less ~/notes/$*
    ls ~/notes

Saving user functions

The easiest way to save a function is to add it to your zsh customizations, typically stored at ~/.zshrc.

You’ll probably want to do that with a text editor, but if you’ve already entered the function into the shell, you can append its definition to ~/.zshrc like this:

print -- "\n\n$(which note)" >> ~/.zshrc

Next steps

There’s certainly room for improvement.

Most notably, the note function doesn’t handle multiple arguments correctly. (Figuring out why is left as an exercise for the reader.) I plan to address that shortcoming, as well as other improvements and enhancements, in a future tutorial.

Meanwhile, please share any related thoughts, corrections, or questions on Twitter.