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.
Goals
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
0
% 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 $#
5
% 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 $#
0
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
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 -- $*
-v
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
-zsh
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
else
print no
fi
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
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/$*
else
ls ~/notes
fi
}
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.