Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Yash-rs Homepage

Yash-rs is a POSIX-compliant command-line shell for Unix-like operating systems, written in Rust. It is designed to be a modern, fast, and extensible shell that provides a rich set of features for users. Currently, it is in the early stages of development and is not yet feature-complete.

Features

  • Shell scripting with POSIX-compliant syntax and semantics
  • Enhanced scripting features (extension to POSIX)
  • Locale support
  • Job control and minimal interactivity
  • Command-line editing and history
  • Tab completion and suggestions

License

This project is licensed under the GNU General Public License v3.0. The full license text can be found at https://github.com/magicant/yash-rs/blob/master/yash-cli/LICENSE-GPL.

Documentation

Navigate the documentation using the left sidebar. If the sidebar is not visible, you can toggle it by clicking the “hamburger” icon in the top-left corner.

Currently, the documentation is a work in progress and may not cover all features or usage scenarios. Contributions are welcome via issues or pull requests to enhance the documentation.

This documentation, hosted at https://magicant.github.io/yash-rs/, is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0). The full license text can be found at https://github.com/magicant/yash-rs/blob/master/docs/LICENSE.

Installation

Downloading precompiled binaries

Precompiled binaries are available for the following platforms:

  • aarch64-unknown-linux-gnu
  • x86_64-unknown-linux-gnu
  • aarch64-apple-darwin
  • x86_64-apple-darwin

You can download the latest release from the GitHub releases page. Download the appropriate binary for your platform and place it in a directory included in your PATH environment variable.

Building from source

Yash-rs is written in Rust, so you need to have the Rust toolchain installed. The recommended way to install Rust is via rustup.

If you are using Windows Subsystem for Linux (WSL), make sure to install the Linux version of rustup, not the native Windows version. For alternative installation methods, refer to the rustup book.

By default, installing rustup also installs the stable Rust toolchain. If the stable toolchain is not installed, you can add it with the following command:

rustup default stable

To install yash-rs, run:

cargo install yash-cli

Running yash-rs

After installation, you can run yash3 from the command line.

Getting started

Starting the shell

To start the shell, run yash3 from the command line. This starts an interactive shell session.

yash3

You will see a prompt, indicating the shell is ready for commands:

$

Using the shell interactively

Once the shell is running, you can type commands. The shell executes each command you enter, and you will see the output in the terminal.

Most commands run a utility, which is a program that performs a specific task. For example, you can run the echo utility to print a message:

$ echo "Hello, world!"
Hello, world!

In this example, $ is the shell prompt, and echo "Hello, world!" is the command you entered. The shell executed the echo utility, which printed “Hello, world!” to the terminal.

You can also run other utilities, such as ls, which lists the files in the working directory:

$ ls
Documents  Downloads  Music  Pictures  Videos

The output varies depending on the files in your working directory.

Interrupting a command

To interrupt a running command, press Ctrl+C. This sends an interrupt signal to the running utility, causing it to terminate. For example, if you run a command that takes a long time, you can cancel it with Ctrl+C:

$ sleep 10

This command sleeps for 10 seconds, but you can interrupt it by pressing Ctrl+C. This aborts the sleep utility and returns you to the shell prompt immediately.

Note: Some utilities may not respond to Ctrl+C if they are designed to ignore or handle the interrupt signal differently.

Exiting the shell

To exit the shell, use the exit command:

$ exit

This ends the shell session and returns you to your previous shell.

Alternatively, you can press Ctrl+D to exit the shell. This sends an empty command to the shell, causing it to exit.

Running scripts

You can also run scripts in the shell. To do this, create a script file with the commands you want to run. For example, create a file called script.sh with the following content:

echo "This is a script"
echo "Running in the shell"

Run this script in the shell by using the . utility:

$ . ./script.sh
This is a script
Running in the shell

You can also run the script by passing it as an argument to the shell:

$ yash3 ./script.sh
This is a script
Running in the shell

This runs the script in a new shell session. The output will be the same.

If you make the script executable, you can run it directly:

$ chmod a+x script.sh
$ ./script.sh
This is a script
Running in the shell

The chmod utility makes the script file executable. This allows you to run the script directly, without specifying the shell explicitly, as in the previous example.

Note the ./ in the commands above. This indicates that the script is in the current directory. If you omit ./, the shell searches for the script in the directories listed in the PATH environment variable. If the script is not in one of those directories, you will get a “utility not found” error.

Shell language

The shell interprets input as commands written in the shell language. The language has a syntax (how commands are written and structured) and semantics (how commands are executed). This page gives a brief overview of shell commands.

Simple commands

A simple command is the most basic command type. It consists of a sequence of words that are not reserved words or operators. For example:

ls

or:

echo "Hello, world!"

Most simple commands run a utility—a program that performs a specific task. The first word is the utility name; the rest are arguments.

All words (except redirection operators) in a simple command are expanded before the utility runs. See Words, tokens, and fields for details on parsing and expansion.

You can use parameters to change command behavior dynamically. There are three types: variables, special parameters, and positional parameters.

See Simple commands for more on assignments, redirections, and command search.

Other commands

Other command types construct more complex behavior by combining commands. See Commands for the full list. For example:

  • Compound commands group commands, control execution, and handle conditions and loops. Examples: if, for, while, case.
  • Pipelines connect the output of one command to the input of another, letting you chain commands.
  • And-or lists control execution flow based on command success or failure.
  • Lists let you run multiple commands in sequence or in parallel.

Functions

Functions are reusable blocks of code you can define and call in the shell. They help organize scripts and interactive sessions.

Redirections

Redirections control where command input and output go. Use them to save output to files or read input from files. See the File descriptors and redirections section for more.

Aliases

Aliases are shortcuts for longer commands or command sequences. They let you create custom names for commands you use often. See the Aliases section for details.

Words, tokens, and fields

In the shell language, a word is a sequence of characters, usually separated by whitespace. Words represent commands, arguments, and other elements in the shell.

In this example, echo, Hello,, and world! are all words:

$ echo Hello, world!
Hello, world!

The first word (echo) is the name of the utility to run. The other words are arguments passed to that utility.

Before running the utility, the shell expands words. This means the shell processes certain characters and sequences in the words to produce the final command line. For example, $ is used for parameter expansion, letting you access variable values:

$ name="Alice"
$ echo Hello, $name!
Hello, Alice!

Here, $name is expanded to its value (Alice) before echo runs.

To prevent expansion, quote the characters you want to treat literally. For example, to print $name without expanding it, use single quotes:

$ echo '$name'
$name

Tokens and operators

A token is a sequence of characters processed as a single unit in shell syntax. The shell divides input into tokens, which are then parsed to form commands. A token is either a word or an operator.

The shell recognizes these operators:

  • Newline – Command separator
  • ; – Command separator
  • & – Asynchronous command
  • && – Logical and
  • || – Logical or
  • | – Pipeline
  • ( – Start of a subshell
  • ) – End of a subshell
  • ;; – End of a case item
  • ;& – End of a case item
  • ;;& – End of a case item
  • ;| – End of a case item
  • < – Input redirection
  • <& – Input redirection
  • <( – Process redirection
  • << – Here document
  • <<- – Here document
  • <<< – Here string
  • <> – Input and output redirection
  • > – Output redirection
  • >& – Output redirection
  • >| – Output redirection
  • >( – Process redirection
  • >> – Output redirection
  • >>| – Pipeline redirection

When recognizing operators, the shell matches the longest possible sequence first. For example, && is a single operator, not two & operators, and <<<< is recognized as <<< and <, not two << operators.

Blank characters (spaces and tabs) separate tokens unless quoted. Words (non-operator tokens) must be separated by at least one blank character. Operators do not need to be separated by blanks if they are recognized as expected.

These two lines are equivalent:

$ ((echo hello))
hello
$ ( ( echo hello ) )
hello

However, you cannot omit the space between ; and ;; in a case command:

$ case foo in (foo) echo foo; ;; esac
foo
$ case foo in (foo) echo foo;;; esac
error: the pattern is not a valid word token
 --> <stdin>:2:29
  |
2 | case foo in (foo) echo foo;;; esac
  |                             ^ expected a word
  |

Word expansion

The shell performs several kinds of word expansion before running a utility, such as replacing parameters with their values or evaluating arithmetic expressions.

The following expansions happen first:

After these, the shell performs these steps in order:

  1. Field splitting
  2. Pathname expansion
  3. Quote removal

The result is a list of words passed to the utility. Each word resulting from these expansions is called a field.

A subset of these expansions are performed depending on the context. For example, when assigning a variable, the shell performs tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal before the assignment. However, field splitting and pathname expansion do not occur during variable assignment, since the value of a variable cannot be split into multiple fields.

Quoting and escaping

Some characters have special meanings in the shell. For example, the dollar sign ($) is used for parameter expansion, and the asterisk (*) is used for pathname expansion. To include these characters literally in a command, you need to quote or escape them.

What characters need quoting?

The following characters have special meanings in the shell and may need quoting or escaping:

|  &  ;  <  >  (  )  $  `  \  "  '

Whitespace characters (spaces, tabs, and newlines) also need quoting or escaping if they are part of a command word.

Additionally, the following characters are treated specially in certain contexts:

*  ?  [  ]  ^  -  !  #  ~  =  %  {  ,  }

It is best to quote or escape these characters when they are used to stand for themselves in a command. You also need to quote reserved words (e.g., if, while, etc.) to treat them as regular words.

The following subsections explain methods for quoting and escaping characters in the shell.

Single quotes

Single quotes enclose a string and prevent the shell from interpreting special characters. Everything inside single quotes is treated literally, including spaces and special characters.

For example, the following command prints the string "$foo" without interpreting the $ as a parameter expansion or the " as a double quote:

$ echo '"$foo"'
"$foo"

Single quotes can contain newline characters:

$ echo 'foo
> bar'
foo
bar

Note that the > prompt indicates that the command continues on the next line.

You cannot include a single quote character inside a single-quoted string. Use double quotes or a backslash to escape it:

$ echo "'"
'
$ echo \'
'

Double quotes

Double quotes enclose a string. Most characters inside double quotes are treated literally, but some characters are still interpreted by the shell:

  • $: Parameter expansion, command substitution, and arithmetic expansion
  • `: Command substitution
  • \: Character escape, only before ", $, `, \, and newline

For example, single quote characters are treated literally and parameter expansion is performed inside double quotes:

$ foo="*  *"
$ echo "foo='$foo'"
foo='*  *'

Double quotes prevent field splitting and pathname expansion on the result of expansions. If the argument to the echo utility were not double-quoted in the above example, the output might have been different depending on the result of field splitting and pathname expansion.

Backslash

The backslash escapes special characters, allowing you to include them in a string without interpretation.

Outside double quotes, a backslash can escape any character except newline. For example:

cat My\ Diary.txt

This prints the contents of the file My Diary.txt.

When used in double quotes, the backslash only escapes the following characters: ", $, `, and \. For example:

cat "My\ Diary\$.txt"

This will print the contents of the file My\ Diary$.txt. Note that the backslash before the space is treated literally, and the backslash before the dollar sign is treated as an escape character.

When used in a braced parameter expansion that occurs inside double quotes, the backslash additionally escapes }:

$ var="{foo}bar"
$ echo "${var#*\}}"
bar

Within backquotes, arithmetic expansions, and unquoted here-document contents, backslashes only escape $, `, and \. If backquotes appear inside double quotes, backslashes also escape ". See examples in the Command substitution and Arithmetic expansion sections.

Line continuation

Line continuation allows you to split long commands into multiple lines for better readability. Use a backslash followed by a newline to indicate that the command continues on the next line. A backslash-newline pair is ignored by the shell as if it were not there. Line continuation can be used inside and outside double quotes, but not inside single quotes.

$ echo "This is a long command that \
> continues on the next line"
This is a long command that continues on the next line

To treat a newline literally rather than as a line continuation, use single or double quotes.

Dollar single quotes

Dollar single quotes ($'…') are used to specify strings with escape sequences, similar to those in C. The content inside the quotes is parsed, and recognized escape sequences are replaced with their corresponding characters.

For example, \n is replaced with a newline character:

$ echo $'foo\nbar'
foo
bar

The following escape sequences are recognized inside dollar single quotes:

  • \\ – backslash
  • \' – single quote
  • \" – double quote
  • \n – newline
  • \t – tab
  • \r – carriage return
  • \a – alert (bell)
  • \b – backspace
  • \e or \E – escape
  • \f – form feed
  • \v – vertical tab
  • \? – question mark
  • \cX – control character (e.g., \cA for ^A)
  • \xHH – byte with hexadecimal value HH (1–2 hex digits)
  • \uHHHH – Unicode character with hexadecimal value HHHH (4 hex digits)
  • \UHHHHHHHH – Unicode character with hexadecimal value HHHHHHHH (8 hex digits)
  • \NNN – byte with octal value NNN (1–3 octal digits)

Unrecognized or incomplete escape sequences cause an error.

A backslash followed by a newline is not treated as a line continuation inside dollar single quotes; they are rejected as an error.

Example with Unicode:

$ echo $'\u3042'
あ

Dollar single quotes are useful for specifying strings with special characters.

In the current implementation, escape sequences that produce a byte are treated as a Unicode character with the same value and converted to UTF-8. This means that byte values greater than or equal to 0x80 are converted to two bytes of UTF-8. This behavior does not conform to the POSIX standard and may change in the future.

Quote removal

When a word is expanded, any quotation marks (single quotes, double quotes, or backslashes used for quoting) that were present in the original command are removed. This process is called quote removal.

For example:

$ echo 'Hello, world!' # the single quotes are removed during expansion
Hello, world!

Quote removal only affects quotes that were part of the original input, not those introduced by expansions:

$ x='\*'
$ echo $x # the backslash is not removed because it was introduced by expansion
\*

Reserved words

Some words have special meaning in shell syntax. These reserved words must be quoted to use them literally. The reserved words are:

  • ! – Negation
  • { – Start of a grouping
  • } – End of a grouping
  • [[ – Start of a double bracket command
  • case – Case command
  • do – Start of a loop or conditional block
  • done – End of a loop or conditional block
  • elif – Else if clause
  • else – Else clause
  • esac – End of a case command
  • fi – End of an if command
  • for – For loop
  • function – Function definition
  • if – If command
  • in – Delimiter for a for loop
  • then – Then clause
  • until – Until loop
  • while – While loop

Currently, [[ and function are only recognized as reserved words; their functionality is not yet implemented.

Additionally, the POSIX standard allows for the following optional reserved words:

  • ]] – End of a double bracket command
  • namespace – Namespace declaration
  • select – Select command
  • time – Time command

These four words are not reserved in yash-rs now, but may be in the future.

Where are reserved words recognized?

Reserved words are recognized in these contexts:

  • As the first word of a command
  • As a word following any reserved word other than case, for, or in
  • in as the third word in a for loop or case command
  • do as the third word in a for loop

Examples

This example uses the reserved words for, in, do, and done in a for loop:

$ for i in 1 2 3; do echo $i; done
1
2
3

In the following example, {, do, and } are not reserved words because they are not the first word of the command:

$ echo { do re mi }
{ do re mi }

Reserved words are recognized only when they appear as a whole word. In this example, { and } are not reserved words because they are part of {echo and Hello}:

$ {echo Hello}
error: cannot execute external utility "{echo"
 --> <stdin>:1:1
  |
1 | {echo Hello}
  | ^^^^^ utility not found
  |

To use { and } as reserved words, write them as separate words:

$ { echo Hello; }
Hello

Comments

Use comments to add notes or explanations to shell scripts and commands. The shell ignores comments while parsing commands.

A comment starts with the # character and continues to the end of the line.

$ # This is a comment
$ echo "Hello, world!"  # This prints a message
Hello, world!

Always separate the start of a comment from the preceding word with whitespace. If there is no whitespace, the shell treats the # as part of the word, not as a comment.

$ echo "Hello, world!"# This is not a comment
Hello, world!# This is not a comment

Everything after # on the same line is ignored by the shell. You cannot use line continuation inside comments.

$ echo one # This backslash is not a line continuation 👉 \
one
$ echo two # So this line is a separate command
two

Tilde expansion

In tilde expansion, the shell replaces a tilde (~) at the start of a word with the value of the HOME variable, allowing you to specify paths relative to your home directory. For example, if HOME is /home/alice:

$ HOME=/home/alice
$ echo ~
/home/alice
$ echo ~/Documents
/home/alice/Documents

The HOME variable is usually passed as an environment variable to the shell when the user logs in, so you don’t need to set it manually.

You can also use ~ followed by a username to refer to another user’s home directory:

$ echo ~bob
/home/bob
$ echo ~bob/Documents
/home/bob/Documents

In variable assignments, tilde expansion happens at the start of the value and after each : character:

$ PATH=~/bin:~bob/bin:~clara/bin:/usr/bin
$ echo "$PATH"
/home/alice/bin:/home/bob/bin:/home/clara/bin:/usr/bin

If tilde expansion produces a pathname ending with / followed by another /, one / is removed:

$ HOME=/
$ echo ~/tmp
/tmp

In older shells, //tmp may be produced instead of /tmp, which can refer to a different location. POSIX.1-2024 now requires the behavior shown above.

Tilde expansion only happens at the start of a word, or after each / (or : in variable assignments). If any part of the expansion or delimiter is quoted, the shell treats them literally:

$ echo ~'b'ob
~bob
$ echo ~\/
~/

Currently, the shell ignores any errors during tilde expansion and leaves the tilde as is. This behavior may change in the future.

The shell may support other forms of tilde expansion in the future, e.g., ~+ for the current working directory.

Parameter expansion

Parameter expansion retrieves the value of a parameter when a command is executed. The basic syntax is ${parameter}.

$ user="Alice" # define a variable
$ echo "Hello, ${user}!" # expand the variable
Hello, Alice!

Unset parameters

If a parameter is unset, the shell expands it to an empty string by default.

$ unset user
$ echo "Hello, ${user}!"
Hello, !

If the nounset shell option is enabled, expanding an unset parameter is an error:

$ set -o nounset
$ echo "Hello, ${user}!"
error: cannot expand unset parameter
 --> <stdin>:2:14
  |
2 | echo "Hello, ${user}!"
  |              ^^^^^^^ parameter `user` is not set
  |
  = info: unset parameters are disallowed by the nounset option

Using nounset is recommended to catch typos in variable names.

Omitting braces

Braces are optional if the parameter is:

  • a variable name with only ASCII letters, digits, and underscores (e.g., $HOME, $user)
  • a special parameter (e.g., $?, $#)
  • a single-digit positional parameter (e.g., $1, $2)

For variable names, the shell uses the longest possible name after $, regardless of whether the variable exists:

$ user="Alice"
$ unset username
$ echo "Hello, $username!" # $user is not considered
Hello, !

For positional parameters, only a single digit is used, even if followed by more digits:

$ set foo bar baz # set three positional parameters
$ echo "$12" # $1 expands to the first positional parameter
foo2

Modifiers

Modifiers change the value of a parameter during expansion. Modifiers can only be used in braced expansions, and only one modifier is allowed per expansion.

Length

The length modifier ${#parameter} returns the number of characters in the parameter’s value.

$ user="Alice"
$ echo "Length of user: ${#user}"
Length of user: 5

As an extension, the length modifier can be used with arrays or special parameters * or @, applying the modifier to each element:

$ users=(Alice Bob Charlie)
$ echo "Lengths of users: ${#users}"
Lengths of users: 5 3 7
$ set yellow red green blue # set four positional parameters
$ echo "Lengths of positional parameters: ${#*}"
Lengths of positional parameters: 6 3 5 4

Switch

The switch modifier changes the result based on whether a parameter is set or empty. There are eight forms:

  • ${parameter-word} – Use word if parameter is unset.
  • ${parameter:-word} – Use word if parameter is unset or empty.
  • ${parameter+word} – Use word if parameter is set.
  • ${parameter:+word} – Use word if parameter is set and not empty.
  • ${parameter=word} – Assign word to parameter if unset, using the new value.
  • ${parameter:=word} – Assign word to parameter if unset or empty, using the new value.
  • ${parameter?word} – Error with word if parameter is unset.
  • ${parameter:?word} – Error with word if parameter is unset or empty.

Examples:

$ user="Alice"
$ echo "Hello, ${user-World}!"
Hello, Alice!
$ unset user
$ echo "Hello, ${user-World}!"
Hello, World!
$ unset PATH
$ PATH="/bin${PATH:+:$PATH}"
$ echo "$PATH"
/bin
$ PATH="/usr/bin${PATH:+:$PATH}"
$ echo "$PATH"
/usr/bin:/bin
$ unset user
$ echo "Hello, ${user=Alice}!"
Hello, Alice!
$ echo "Hello, ${user=Bob}!"
Hello, Alice!
$ user="Alice"
$ echo "Hello, ${user?tell me your name}!"
Hello, Alice!
$ unset user
$ echo "Hello, ${user?tell me your name}!"
error: tell me your name
 --> <stdin>:4:14
  |
4 | echo "Hello, ${user?tell me your name}!"
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^ parameter `user` is not set
  |

In all cases, the following expansions are performed on word before use:

For the = and := forms, quote removal is also performed before assignment. Assignment only works for variables, not special or positional parameters.

If word is empty in the ? and :? forms, a default error message is used.

The nounset option does not apply to expansions with a switch modifier.

Trim

The trim modifier removes leading or trailing characters matching a pattern from a parameter’s value. There are four forms:

  • ${parameter#pattern} – Remove the shortest match of pattern from the start.
  • ${parameter##pattern} – Remove the longest match of pattern from the start.
  • ${parameter%pattern} – Remove the shortest match of pattern from the end.
  • ${parameter%%pattern} – Remove the longest match of pattern from the end.

The value is matched against the pattern, and the matching part is removed.

$ var="banana"
$ echo "${var#*a}"
nana
$ echo "${var##*a}"

$ echo "${var%a*}"
banan
$ echo "${var%%a*}"
b

The pattern is expanded before use:

You can quote part or all of the pattern to treat it literally:

$ asterisks="***"
$ echo "${asterisks##*}" # removes the whole value

$ echo "${asterisks##\*}" # removes the first *
**
$ echo "${asterisks##'**'}" # removes the first two *
*

Compatibility

Some modifiers are ambiguous when used with a certain special parameter. Yash and many other shells interpret ${##}, ${#-}, and ${#?} as length modifiers applied to special parameters #, -, and ?, not as switch or trim modifiers applied to #. The POSIX standard is unclear on this point.

The result is unspecified in POSIX for:

  • a length or switch modifier applied to special parameter * or @
  • a trim modifier applied to special parameter #, *, or @

Command substitution

Command substitution expands to the output of a command. It has two forms: the preferred $(command) form and the deprecated backquote form `command`.

For example, this runs dirname -- "$0" and passes its output to cd:

$ cd -P -- "$(dirname -- "$0")"

This changes the working directory to the directory containing the script, regardless of the current directory.

Syntax

The $(…) form evaluates the command inside the parentheses. It supports nesting and is easier to read than backquotes:

$ echo $(echo $(echo hello))
hello
$ echo "$(echo "$(echo hello)")"
hello

In the backquote form, backslashes escape $, `, and \. If backquotes appear inside double quotes, backslashes also escape ". These escapes are processed before the command is run. A backquote-form equivalent to the previous example is:

$ echo `echo \`echo hello\``
hello
$ echo "`echo \"\`echo hello\`\"`"
hello

The $(…) form can be confused with arithmetic expansion. Command substitution is only recognized if the code is not a valid arithmetic expression. For example, $((echo + 1)) is arithmetic expansion, but $((echo + 1); (echo + 2)) is command substitution. To force command substitution starting with a subshell, insert a space: $( (echo + 1); (echo + 2)).

Semantics

The command runs in a subshell, and its standard output is captured. Standard error is not captured unless redirected. Trailing newlines are removed, and the result replaces the command substitution in the command line.

Currently, yash-rs parses the command when the substitution is executed, not when it is parsed. This may change in the future, affecting when syntax errors are detected and when aliases are substituted.

Arithmetic expansion

Arithmetic expansion evaluates an arithmetic expression and replaces it with the result. The syntax is $((expression)).

$ echo $((1 + 2))
3
$ echo $((2 * 3 + 4))
10
$ echo $((2 * (3 + 4)))
14

Arithmetic expansion works in two steps. First, the expression is processed for parameter expansion, nested arithmetic expansion, command substitution, and quote removal. Then, the resulting string is parsed as an arithmetic expression, and the result replaces the expansion.

$ x=2
$ echo $(($x + $((3 * 4)) + $(echo 5)))
19

Variables

The value of variables appearing as parameter expansions does not have to be numeric, but the resulting arithmetic expression must be valid.

$ seven=7
$ var='6 * sev'
$ echo $((${var}en)) # expands to $((6 * seven))
42
$ seven='3 + 4'
$ echo $((2 * $seven)) # expands to $((2 * 3 + 4)), mind the precedence
10
$ echo $((2 * seven))
error: error evaluating the arithmetic expansion
 --> <arithmetic_expansion>:1:5
  |
1 | 2 * seven
  |     ^^^^^ invalid variable value: "3 + 4"
  |
 ::: <stdin>:3:6
  |
3 | echo $((2 * seven))
  |      -------------- info: arithmetic expansion appeared here
  |

Quoting

Backslash escaping is the only supported quoting mechanism in arithmetic expansion. It can escape $, `, and \. However, escaped characters would never produce a valid arithmetic expression after quote removal, so they are not useful in practice.

$ echo $((\$x))
error: error evaluating the arithmetic expansion
 --> <arithmetic_expansion>:1:1
  |
1 | $x
  | ^ invalid character
  |
 ::: <stdin>:1:6
  |
1 | echo $((\$x))
  |      -------- info: arithmetic expansion appeared here
  |

Field splitting

Field splitting breaks a word into fields at delimiters. This happens after parameter expansion, command substitution, and arithmetic expansion, but before pathname expansion and quote removal.

In this example, $flags is split at the space, so ls receives two arguments:

$ flags='-a -l'
$ ls $flags
total 10468
drwxr-xr-x  2 user group    4096 Oct 10 12:34 Documents
drwxr-xr-x  3 user group    4096 Oct 10 12:34 Downloads
drwxr-xr-x  3 user group    4096 Oct 10 12:34 Music
drwxr-xr-x  2 user group    4096 Oct 10 12:34 Pictures
drwxr-xr-x  2 user group    4096 Oct 10 12:34 Videos

Field splitting does not occur if the expansion is quoted:

$ flags='-a -l'
$ ls "$flags"
ls: invalid option -- ' '
Try 'ls --help' for more information.

Field splitting only applies to the results of parameter expansion, command substitution, and arithmetic expansion, not to literals or tilde expansions:

$ HOME='/home/user/My Documents'
$ ls ~
Documents  Downloads  Music  Pictures  Videos
$ ls "$HOME"
Documents  Downloads  Music  Pictures  Videos
$ ls $HOME
ls: cannot access '/home/user/My': No such file or directory
ls: cannot access 'Documents': No such file or directory

Field splitting only happens where words are expected, such as simple command words, for loop words, and array assignments. It does not occur in contexts expecting a single word, like scalar assignments or case patterns.

$ flags='-a -l'
$ oldflags=$flags # no field splitting; oldflags is '-a -l'
$ flags="$flags -r"
$ ls $flags # field splitting; ls receives '-a', '-l', and '-r'
Videos  Pictures  Music  Downloads  Documents
$ flags=$oldflags # again, no field splitting
$ echo "Restored flags: $flags"
Restored flags: -a -l

IFS

Field splitting is controlled by the IFS (Internal Field Separator) variable, which lists delimiter characters. By default, IFS contains a space, tab, and newline. You can change IFS to use different delimiters.

If IFS is unset, the default value is used:

$ unset IFS
$ flags='-a -l'
$ ls $flags
total 10468
drwxr-xr-x  2 user group    4096 Oct 10 12:34 Documents
drwxr-xr-x  3 user group    4096 Oct 10 12:34 Downloads
drwxr-xr-x  3 user group    4096 Oct 10 12:34 Music
drwxr-xr-x  2 user group    4096 Oct 10 12:34 Pictures
drwxr-xr-x  2 user group    4096 Oct 10 12:34 Videos

If IFS is set to an empty string, no splitting occurs:

$ IFS=''
$ flags='-a -l'
$ ls $flags
ls: invalid option -- ' '
Try 'ls --help' for more information.

Each character in IFS is a delimiter. How fields are split depends on whether a delimiter is whitespace or not.

Non-whitespace delimiters split the word at their position and may produce empty fields:

$ IFS=':'
$ values='a:b::c:d'
$ for value in $values; do echo "[$value]"; done
[a]
[b]
[]
[c]
[d]

Empty fields are not produced after a trailing non-whitespace delimiter:

$ IFS=':'
$ values='a:b:'
$ for value in $values; do echo "[$value]"; done
[a]
[b]

Whitespace delimiters also split the word, but do not produce empty fields. Multiple whitespace delimiters in a row are treated as one:

$ IFS=' '
$ values=' a  b   c'
$ for value in $values; do echo "[$value]"; done
[a]
[b]
[c]

Whitespace and non-whitespace delimiters can be combined in IFS:

$ IFS=' :'
$ values='a:b  c : d:  :e  f '
$ for value in $values; do echo "[$value]"; done
[a]
[b]
[c]
[d]
[]
[e]
[f]

Currently, yash-rs only supports UTF-8 encoded text. This does not fully conform to POSIX, which requires handling arbitrary byte sequences.

Empty field removal

During field splitting, empty fields are removed, except those delimited by non-whitespace IFS characters.

$ empty='' space=' '
$ for value in $empty; do echo "[$value]"; done # prints nothing
$ for value in $space; do echo "[$value]"; done # prints nothing

Empty fields are removed even if IFS is empty:

$ IFS=''
$ empty='' space=' '
$ for value in $empty; do echo "[$value]"; done # prints nothing
$ for value in $space; do echo "[$value]"; done # prints one field containing a space
[ ]

To retain empty fields, quote the word to prevent field splitting:

$ empty='' space=' '
$ for value in "$empty"; do echo "[$value]"; done
[]
$ for value in "$space"; do echo "[$value]"; done
[ ]

Pathname expansion (globbing)

Pathname expansion—also known as globbing—lets you use special patterns to match filenames and directories. The shell expands these patterns into a list of matching pathnames.

For example, *.txt matches all files in the current directory ending with .txt:

$ echo *.txt
notes.txt todo.txt

When does pathname expansion happen?

Pathname expansion occurs after field splitting and before quote removal. It only applies to unquoted words containing globbing characters.

If the noglob shell option is set, pathname expansion is skipped.

Pattern syntax

Pathname expansion uses shell patterns. Patterns may include special characters such as *, ?, and bracket expressions. See Pattern matching for a full description of pattern syntax and matching rules.

The following subsections describe aspects of pattern matching specific to pathname expansion.

Unmatched brackets

Unmatched brackets like [a and b]c are treated as literals. However, some shells may treat other glob characters as literals if they are used with unmatched open brackets. To avoid this, make sure to quote unmatched open brackets:

$ echo [a
[a
$ echo \[*
[a [b [c

Subdirectories

Globs do not match / in filenames. To match files in subdirectories, include / in the pattern:

$ echo */*.txt
docs/readme.txt notes/todo.txt

Brackets cannot contain / because patterns are recognized for each component separated by /. For example, the pattern a[/]b only matches the literal pathname a[/]b, not a/b, because the brackets are considered unmatched in the sub-patterns a[ and ]b.

Hidden files

By default, glob patterns do not match files starting with a dot (.). To match hidden files, the pattern must start with a literal dot:

$ echo .*.txt
.hidden.txt

Glob patterns never match the filenames . and .., even if a pattern begins with a literal dot.

$ echo .*
.backup.log .hidden.txt

No matches

If a pattern does not match any files, it is left unchanged. If the shell does not have permission to read the directory, the pattern is also left unchanged.

Summary

  • Globbing expands patterns to matching pathnames.
  • See Pattern matching for pattern syntax and details.
  • Quote or escape glob characters to use them literally.
  • Patterns that match nothing are left unchanged.

Parameters

A parameter is a name-value pair used to store and retrieve data in a shell script. Parameters can be variables, special parameters, or positional parameters.

Parameter expansion retrieves the value of a parameter when the command is executed.

$ name="Alice" # define a variable
$ echo "Hello, $name!" # expand the variable
Hello, Alice!

Variables

Variables are parameters with alphanumeric names that can be assigned values. Use variable assignment to define a variable by specifying a name and value.

Variable names

Variable names can contain letters, digits, and underscores, but cannot start with a digit. Variable names are case-sensitive, so VAR and var are different variables.

It is common to use uppercase letters for exported (environment) variables and lowercase for local variables. This avoids accidentally overwriting environment variables.

According to POSIX.1-2024, only ASCII letters, digits, and underscores are portably accepted in variable names. Many shells allow additional characters. Yash-rs currently accepts Unicode letters and digits in variable names, but this may change in the future.

Defining variables

To define a variable, use the assignment syntax:

$ user=Alice

This creates a variable named user with the value Alice. There must be no spaces around the = sign.

Before a value is assigned, the following expansions are performed, if any:

$ HOME=/home/alice
$ topdir=~/my_project
$ subdir=$topdir/docs
$ echo $subdir
/home/alice/my_project/docs
$ rawdir='~/$user'
$ echo $rawdir
~/$user

Note that field splitting and pathname expansion do not happen during assignment.

$ star=* # assigns a literal `*` to the variable `star`
$ echo "$star" # shows the value of `star`
*
$ echo $star # unquoted, the value is subject to field splitting and pathname expansion
Documents  Downloads  Music  Pictures  Videos

See Simple commands for more on assignment behavior.

Environment variables

Environment variables are variables exported to child processes. To export a variable, use the export built-in:

$ export user=Alice
$ sh -c 'echo $user'
Alice

When the shell starts, it inherits environment variables from its parent. These are automatically exported to child processes.

Read-only variables

The readonly built-in makes a variable read-only, preventing it from being modified or unset. This is useful for defining constants.

$ readonly pi=3.14
$ pi=3.14159
error: error assigning to variable
 --> <stdin>:2:1
  |
2 | pi=3.14159
  | ^^^^^^^^^^ cannot assign to read-only variable "pi"
  |
 ::: <stdin>:1:10
  |
1 | readonly pi=3.14
  |          ------- info: the variable was made read-only here
  |

Variables are read-only only in the current shell session. Exported environment variables are not read-only in child processes.

Local variables

Variables defined by the typeset built-in (without --global) are local to the current shell function. Local variables are removed when the function returns. This helps avoid name conflicts and keeps temporary variables out of the global namespace.

$ i=0
$ list() {
>   typeset i
>   for i in 1 2 3; do
>     echo "Inside function: $i"
>   done
> }
$ list
Inside function: 1
Inside function: 2
Inside function: 3
$ echo "Outside function: $i"
Outside function: 0

The original (global) variable is hidden by the local variable inside the function and restored when the function returns.

Variables have dynamic scope: functions can access local variables defined in the function that called them, as well as global variables.

$ outer() {
>     typeset user="Alice"
>     inner
>     echo "User in outer: $user"
> }
$ inner() {
>     echo "User in inner: ${user-not set}"
>     user="Bob"
> }
$ outer
User in inner: Alice
User in outer: Bob
$ echo "User in global scope: ${user-not set}"
User in global scope: not set
$ inner
User in inner: not set
$ echo "User in global scope: ${user-not set}"
User in global scope: Bob

In this example, inner called from outer accesses the local variable user defined in outer. The value is changed in inner, and this change is visible in outer after inner returns. After outer returns, the local variable no longer exists. When inner is called directly, it creates a new global variable user.

Removing variables

The unset built-in removes a variable.

$ user=Alice
$ echo user=$user
user=Alice
$ unset user
$ echo user=$user
user=

Undefined variables by default expand to an empty string. Use the -u shell option to make the shell treat undefined variables as an error.

Reserved variable names

Some variable names are reserved for special purposes. These variables may affect or be affected by the shell’s behavior.

  • CDPATH: A colon-separated list of directories to search in the cd built-in

  • ENV: The name of a file to be sourced when starting an interactive shell

  • HOME: The user’s home directory, used in tilde expansion

  • IFS: A list of delimiters used in field splitting

    • The default value is a space, tab, and newline.
  • LINENO: The current line number in the shell script

    • This variable is automatically updated as the shell executes commands.
    • Currently, yash-rs does not support exporting this variable.
  • OLDPWD: The previous working directory, updated by the cd built-in

  • OPTARG: The value of the last option argument processed by the getopts built-in

  • OPTIND: The index of the next option to be processed by the getopts built-in

  • PATH: A colon-separated list of directories to search for executable files when running external utilities

  • PPID: The process ID of the parent process of the shell

    • This variable is initialized when the shell starts.
  • PS1: The primary prompt string, displayed before each command in interactive mode

    • The default value is $ (a dollar sign followed by a space).
  • PS2: The secondary prompt string, displayed when a command is continued on the next line

    • The default value is > (a greater-than sign followed by a space).
  • PS4: The pseudo-prompt string, used for command execution tracing

    • The default value is + (a plus sign followed by a space).
  • PWD: The current working directory

    • This variable is initialized to the working directory when the shell starts and updated by the cd built-in when changing directories.

Arrays

Arrays are variables that can hold multiple values.

Defining arrays

To define an array, wrap the values in parentheses:

$ fruits=(apple banana cherry)

Accessing array elements

Accessing individual elements is not yet implemented in yash-rs.

To access all elements, use the array name in parameter expansion:

$ fruits=(apple banana cherry)
$ for fruit in "$fruits"; do echo "$fruit"; done
apple
banana
cherry

Special parameters

Special parameters are predefined parameters that have symbolic names and provide specific information about the shell environment. They are not user-defined variables and cannot be assigned values with the assignment syntax.

Below are the special parameters and their meanings:

  • @: All positional parameters.

    • Expands to all positional parameters as separate fields. Useful for passing all arguments as is to a utility or function.
    • When expanded outside double quotes, the result is subject to field splitting and pathname expansion. To preserve each parameter as a separate field, use "$@". If there are no positional parameters, "$@" expands to zero fields.
    • In contexts where only one field is expected (such as in the content of a here-document), @ expands to a single field with all positional parameters joined by the first character of the IFS variable (defaults to space if unset, or no separator if IFS is empty).
    $ set foo 'bar bar' baz # three positional parameters
    $ for value in "$@"; do echo "[$value]"; done
    [foo]
    [bar bar]
    [baz]
    $ for value in $@; do echo "[$value]"; done
    [foo]
    [bar]
    [bar]
    [baz]
    
  • *: All positional parameters.

    • Similar to @, but in double quotes, * expands to a single field containing all positional parameters joined by the first character of IFS.
    $ set foo 'bar bar' baz # three positional parameters
    $ for value in "$*"; do echo "[$value]"; done
    [foo bar bar baz]
    $ for value in $*; do echo "[$value]"; done
    [foo]
    [bar]
    [bar]
    [baz]
    
  • #: Number of positional parameters.

    $ set foo 'bar bar' baz
    $ echo "$#"
    3
    
  • ?: Exit status of the last command.

  • -: Current shell options.

    • Expands to the short names of all currently set shell options, concatenated together. Options without a short name are omitted. For example, if -i and -m are set, the value is im.
  • $: Process ID of the current shell.

    • Set when the shell starts and remains constant, even in subshells.
  • !: Process ID of the last asynchronous command.

    • Updated when an asynchronous command is started or resumed.
  • 0: Name of the shell or script being executed.

    • Set at shell startup and remains constant.
    • If neither the -c nor -s shell option is active, the value of 0 is the first operand in the shell invocation (the script pathname).
    • If the -c option is used and a second operand is present, that operand is used as 0.
    • Otherwise, 0 is set to the first argument passed to the shell, usually the shell’s name.

Positional parameters

Positional parameters are parameters identified by their position in the command line. They are commonly used to pass arguments to scripts or functions.

Initializing positional parameters

Positional parameters are set when the shell starts:

  • If neither the -c nor -s shell option is active, positional parameters are set to the operands after the first operand in the shell invocation. For example:

    yash3 script.sh arg1 arg2 arg3
    

    Here, the positional parameters are arg1, arg2, and arg3.

  • If the -c option is used, positional parameters are set to operands after the second operand, if any:

    yash3 -c 'echo "$1" "$2"' arg0 arg1 arg2
    

    The positional parameters are arg1 and arg2. The second operand (arg0) is used as special parameter 0, not as a positional parameter.

  • If the -s option is active, positional parameters are set to all operands in the shell invocation:

    yash3 -s arg1 arg2 arg3
    

    The positional parameters are arg1, arg2, and arg3.

Modifying positional parameters

To set positional parameters, use the set built-in:

$ set foo bar baz
$ echo "$1" "$2" "$3"
foo bar baz

To append new parameters without removing existing ones, use set -- "$@" followed by the new parameters:

$ set old_param1 old_param2
$ set -- "$@" new_param1 new_param2
$ echo "$1" "$2" "$3" "$4"
old_param1 old_param2 new_param1 new_param2

The -- marks the end of options, so parameters starting with - are not treated as options.

To remove the first N positional parameters, use shift:

$ set foo bar baz qux
$ echo "$1" "$2" "$3" "$4"
foo bar baz qux
$ shift 2
$ echo "$1" "$2"
baz qux

If set is called with no operands, positional parameters are unchanged. To clear them, use set -- or shift "$#".

When a function is called, positional parameters are set to the function’s arguments. You can modify them within the function using set or shift. After the function returns, the original positional parameters are restored.

Expanding positional parameters

In parameter expansion, positional parameters are referenced by their position, starting from 1:

$ set foo bar baz
$ echo "$3" "$2" "$1"
baz bar foo

For positions above 9, use braces:

$ set a b c d e f g h i j k l m n o p q r s t u v w x y z
$ echo "${1}" "${10}" "${26}"
a j z
$ echo "$10" # expands as ${1}0
a0

To expand all positional parameters at once, you can use the special parameter @ or *. Specifically, to pass all positional parameters intact to a utility or function, expand @ in double quotes:

$ set foo 'bar bar' baz
$ printf '[%s]\n' "$@"
[foo]
[bar bar]
[baz]

To get the number of positional parameters, use the special parameter #:

$ set foo bar baz
$ echo "$#"
3

Parsing positional parameters

To parse positional parameters as options and arguments, use the getopts built-in. This is useful for scripts that handle command-line options:

$ set -- -a arg1 -b arg2 operand1 operand2
$ while getopts a:b: opt; do
>   case "$opt" in
>     (a)
>       echo "Option -a with argument: $OPTARG"
>       ;;
>     (b)
>       echo "Option -b with argument: $OPTARG"
>       ;;
>     (*)
>       echo "Unknown option: $opt"
>       ;;
>   esac
> done
Option -a with argument: arg1
Option -b with argument: arg2
$ shift $((OPTIND - 1)) # remove parsed options
$ echo "Remaining operands:" "$@"
Remaining operands: operand1 operand2

Commands

This section summarizes the syntax of commands in the shell language. Commands (in the broad sense) are instructions to the shell to perform actions such as running programs, changing the environment, or controlling execution flow. For details, see the linked sections below.

Whole scripts

A shell script consists of a sequence of lists separated by newlines. The shell reads and parses input line by line until it forms a complete list, executes that list, then continues to the next.

$ echo "Hello, World!"
Hello, World!
$ for fruit in apple banana cherry; do
>     echo "I like $fruit"
> done
I like apple
I like banana
I like cherry

Lists

A list is a sequence of and-or lists separated by ; or &. Lists let you write multiple commands on one line or run commands asynchronously.

$ echo "Hello"; echo "World"
Hello
World

And-or lists

An and-or list is a sequence of pipelines separated by && or ||. This lets you control execution flow based on the success or failure of previous commands.

$ test -f /nonexistent/file && echo "File exists" || echo "File does not exist"
File does not exist

Pipelines

A pipeline is a sequence of commands connected by |, where the output of one command is passed as input to the next. Pipelines let you combine commands to process data in a stream.

You can prefix a pipeline with the ! reserved word to negate its exit status:

$ ! tail file.txt | grep TODO
TODO: Fix this issue

Commands

A command (in the narrow sense) is a pipeline component: a simple command, a compound command, or a function definition.

A simple command runs a utility or function, or assigns values to variables.

Compound commands control execution flow and include:

  • Grouping commands: Group multiple commands to run as a unit, in the current shell or a subshell.
  • If commands: Run commands conditionally based on exit status.
  • Case commands: Run commands based on pattern matching a value.
  • For loops: Iterate over a list, running commands for each item.
  • While loops: Repeat commands while a condition is true.
  • Until loops: Repeat commands until a condition becomes true.

A function definition creates a reusable block of code that can be invoked by name.

Simple commands

Simple commands are the basic building blocks of shell commands. They consist of command line words, assignments, and redirections.

Outline

Despite the name, simple commands can have complex behavior. This section summarizes the main aspects before covering details.

Most simple commands run a utility—a program that performs a specific task. A simple command that runs a utility contains a word specifying the utility name, followed by zero or more words as arguments:

$ echo "Hello!"
Hello!

The words are expanded before the utility runs.

A simple command can assign values to variables. To assign a value, join the variable name and value with an equals sign (=) without spaces:

$ greeting="Hello!"
$ echo "$greeting"
Hello!

If assignments are used with utility-invoking words, they must appear before the utility name and they usually affect only that utility invocation:

$ TZ='America/Los_Angeles' date
Sun Jun  1 06:49:30 AM PDT 2025
$ TZ='Asia/Tokyo' date
Sun Jun  1 10:49:30 PM JST 2025

A simple command can also redirect input and output using redirection operators. For example, to redirect output to a file:

$ echo "Hello!" > output.txt
$ cat output.txt
Hello!

Redirections can appear anywhere in a simple command, but are typically placed at the end.

Syntax

The formal syntax of a simple command, written in Extended Backus-Naur Form (EBNF):

simple_command            := normal_utility - reserved_word, [ normal_argument_part ] |
                             declaration_utility, [ declaration_argument_part ] |
                             assignment_part, [ utility_part ] |
                             "command", [ utility_part ];
utility_part              := normal_utility, [ normal_argument_part ] |
                             declaration_utility, [ declaration_argument_part ] |
                             "command", [ utility_part ];
assignment_part           := ( assignment_word | redirection ), [ assignment_part ];
assignment_word           := ? a word that starts with a literal variable name
                               immediately followed by an unquoted equals sign and
                               optionally followed by a value part ?;
command_name              := word - assignment_word;
declaration_utility       := "export" | "readonly" | "typeset";
normal_utility            := command_name - declaration_utility - "command";
normal_argument_part      := ( word | redirection ), [ normal_argument_part ];
declaration_argument_part := ( assignment_word | word - assignment_word | redirection ),
                             [ declaration_argument_part ];

Key points:

  • A simple command cannot start with a reserved word unless it is quoted.
  • An assignment word must start with a non-empty variable name, but the value can be empty.
  • To treat a word containing = as a command name, quote the variable name or the equals sign.
  • Redirections can appear anywhere in a simple command.

There must be no space around the equals sign in an assignment word. If you need spaces in the value, quote them:

$ greeting="Hello, world!"
$ echo "$greeting"
Hello, world!

The utility names export, readonly, and typeset are declaration utilities; when used as a command name, following argument words are parsed as assignment words if possible, or as normal words otherwise. This affects how arguments are expanded. The utility name command is also special; it delegates to the next word the determination of whether it is a declaration utility or a normal utility. (More utility names may be treated as declaration utilities in the future.)

Semantics

A simple command is executed by the shell in these steps:

  1. Command words (name and arguments) are expanded in order.
    • If the command name is a declaration utility, argument words that look like assignment words are expanded as assignments; the rest are expanded as normal words. Otherwise, all arguments are expanded as normal words.
    • Command words are expanded before assignments, so assignments do not affect command words in the same command.
    • Expansions in assignments and redirections are not yet performed in this step.
    • The result is a sequence of words called fields.
    • If expansion fails, the error is reported and the command is aborted.
  2. Redirections are performed, in order.
    • If there are any fields, redirections are processed in the current shell environment. If a redirection fails, the error is reported and the command is aborted.
    • If there are no fields, redirections are processed in a subshell. In this case, redirections do not affect the current shell, and errors are reported but do not abort the command.
  3. Assignments are performed, in order.
    • Each assignment value is expanded and assigned to the variable in the current shell environment. See Defining variables for details.
    • Assigned variables are exported if there are any fields or if the allexport option is enabled.
    • If an assigned variable is read-only, the error is reported and the command is aborted.
  4. If there are any fields, the shell determines the target to execute based on the first field (the command name), as described in Command search below.
  5. The shell executes the target:
    • If the target is an external utility, it is executed in a subshell with the fields as arguments. If the execve call used to execute the target fails with ENOEXEC, the shell tries to execute it as a script in a new shell process.
    • If the target is a built-in, it is executed in the current shell environment with the fields (except the first) as arguments.
    • If the target is a function, it is executed in the current shell environment. When entering a function, positional parameters are set to the fields (except the first), and restored when the function returns.
    • If no target is found, the shell reports an error.
    • If there was no command name (the first field), nothing is executed.

Assigned variables are removed unless the target was a special built-in or there were no fields after expansion, in which case the assignments persist. Redirections are canceled unless the target was the exec special built-in (or the command built-in executing exec), in which case the redirections persist.

Command search determines the target to execute based on the command name (the first field):

  1. If the command name contains a slash (/), it is treated as a pathname to an executable file target, regardless of whether the file exists or is executable.
  2. If the command name is a special built-in (like exec or exit), it is used as the target.
  3. If the command name is a function, it is used as the target.
  4. If the command name is a built-in other than a substitutive built-in, it is used as the target.
  5. The shell searches for the command name in the directories listed in the PATH variable. The first matching executable regular file is a candidate target.
    • The value of PATH is treated as a sequence of pathnames separated by colons (:). An empty pathname in PATH refers to the current directory. For example, in the simple command PATH=/bin:/usr/bin: ls, the shell searches for ls in /bin, then /usr/bin, and finally the current directory.
    • If PATH is an array, each element is a pathname to search.
  6. If a candidate target is found:
    • If the command name is a substitutive built-in (like echo or pwd), the built-in is used as the target.
    • Otherwise, the executable file is used as the target.
  7. If no candidate target is found, the command search fails.

An executable file target is called an external utility.

Exit status

  • If a target was executed, the exit status of the simple command is the exit status of the target.
  • If there were no fields after expansion, the exit status is that of the last command substitution in the command, or zero if there were none.
  • If the command was aborted due to an error in expansion, redirection, assignment, or command search, the exit status is non-zero: 127 if command search failed, or 126 if the target could not be executed (e.g., unsupported file type or permission denied).

Pipelines

A pipeline is a sequence of commands connected by pipes (|). The output of each command is passed as input to the next, allowing you to chain commands for more complex tasks.

Basic usage

The syntax for a pipeline is:

command1 | command2 | command3 …

For example, to list files and filter the output:

$ mkdir $$ && cd $$ || exit
$ > foo.txt > bar.txt > baz.png
$ ls | grep .txt
bar.txt
foo.txt

The | operator may be followed by linebreaks for readability:

$ mkdir $$ && cd $$ || exit
$ > foo.txt > bar.txt > baz.png
$ ls |
> grep .txt |
> wc -l
2

Line continuation can also be used to split pipelines across multiple lines:

$ mkdir $$ && cd $$ || exit
$ > foo.txt > bar.txt > baz.png
$ ls \
> | grep .txt \
> | wc -l
2

If a pipeline contains only one command, the shell runs that command directly. For multiple commands, the shell creates a subshell for each and connects them with pipes. Each command’s standard output is connected to the next command’s standard input. The first command’s input and the last command’s output are not changed. All commands in the pipeline run concurrently. (See What are file descriptors? for more on standard input and output.)

The shell waits for all commands in the pipeline to finish before proceeding. The exit status of the pipeline is the exit status of the last command in the pipeline. (In the future, yash-rs may only wait for the last command to finish.)

Negation

You can negate a pipeline using the ! reserved word:

$ mkdir $$ && cd $$ || exit
$ ! ls | grep .zip
$ echo $?
0

This runs the pipeline and negates its exit status: if the status is 0 (success), it becomes 1 (failure); if non-zero (failure), it becomes 0 (success). This is useful for inverting the result of a command in a conditional.

Negation applies to the pipeline as a whole, not to individual commands. To negate a specific command, use braces:

$ mkdir $$ && cd $$ || exit
$ ls | { ! grep .zip; } && echo "No zip files found"
No zip files found

Since ! is a reserved word, it must appear as a separate word:

$ !ls | grep .zip
error: cannot execute external utility "!ls"
 --> <stdin>:1:1
  |
1 | !ls | grep .zip
  | ^^^ utility not found
  |

Compatibility

POSIX requires that a pipeline waits for the last command to finish before returning an exit status, and it is unspecified whether the shell waits for all commands in the pipeline to finish. yash-rs currently waits for all commands, but this may change in the future.

POSIX allows commands in a multi-command pipeline to be run in the current shell environment rather than in subshells. Korn shell and zsh run the last command in the current shell environment, while yash-rs runs all commands in subshells.

Some shells like Korn shell and mksh assign special meanings to the ! reserved word immediately followed by the ( operator. For maximum compatibility, ! and ( should be separated by a space.

Grouping

A grouping command combines multiple commands so they are treated as a single command. This is useful for running several commands together in a pipeline or an and-or list.

Braces

Commands grouped in braces { … } run in the current shell environment.

$ { echo "Hello"; echo "World"; }
Hello
World

A group can span multiple lines:

$ {
> echo "Hello"
> echo "World"
> }
Hello
World

Since { and } are reserved words, they must appear as separate words. See examples in the Keywords section.

Braces are especially useful for treating several commands as a single unit in pipelines or and-or lists:

$ { echo "Hello"; echo "World"; } | grep "Hello"
Hello
$ HOME=$PWD
$ test -f ~/cache/file || { mkdir -p ~/cache; > ~/cache/file; }

Subshells

Commands grouped in parentheses ( … ) run in a subshell—a copy of the current shell environment. Changes made in a subshell do not affect the parent shell.

$ greeting="Morning"
$ (greeting="Hello"; echo "$greeting")
Hello
$ echo "$greeting"
Morning

Since ( and ) are operators, they can be used without spaces.

Compatibility

Some shells treat two adjacent ( characters specially. For best compatibility, separate open parentheses with a space to nest subshells:

$ ( (echo "Hello"))
Hello

Exit status and conditionals

This section describes the exit status of commands and how to use it to control the flow of execution in the shell.

Exit status

The exit status of a command is a number that indicates an abstract result of the command’s execution. It is used to determine whether a command succeeded or failed.

The value of the exit status is a non-negative integer, typically in the range of 0 to 255. The exit status is stored in the special parameter ? immediately after a command runs.

$ test -f /nonexistent/file
$ echo $? # the exit status of `test`
1
$ echo $? # the exit status of the previous `echo`
0

Before running a command, the initial value of ? is 0.

While the exact meaning of exit status values is specific to each command, there are some common conventions:

  • An exit status of 0 indicates success.
  • A non-zero exit status indicates failure or an error condition. The specific value can provide additional information about the type of failure.
  • Exit statuses in the range of 1 to 125 are generally used by commands to indicate various types of errors or conditions.
  • Exit statuses 126 and greater are reserved by the shell for special purposes.

The following exit statuses are used by the shell to indicate specific conditions:

  • Exit status 126 indicates that a command was found but could not be executed.
  • Exit status 127 indicates that a command was not found.
  • Exit status 128 indicates that the shell encountered an unrecoverable error reading a command.
  • When a command is terminated by a signal, the exit status is 384 plus the signal number. For example, if a command is terminated by SIGINT (signal number 2), the exit status will be 386.

Compatibility of exit statuses

POSIX specifies exit statuses for certain conditions, but there are still many conditions for which POSIX does not define exact exit statuses. Different shells and commands may use different exit statuses for the same conditions, so it’s important to check the documentation of the specific command you are using. Specifically, the exit status of a command terminated by a signal may vary between shells as POSIX only specifies that the exit status must be greater than 128.

Yash-rs internally handles exit statuses as 32-bit signed integers, but receives only the lower 8 bits from child processes running a subshell or external utility. This means that exit statuses that are not in the range of 0 to 255 are truncated to fit into this range. For example, an exit status of 256 becomes 0, and an exit status of 1000 becomes 232.

Exit status of the shell

When exiting a shell, the exit status of the shell itself is determined by the exit status of the last command executed in the shell. If no commands have been executed, the exit status is 0.

If the exit status of the last command indicates that the command was terminated by a signal, the shell sends the same signal to itself to terminate. The parent process (which may or may not be a shell) will observe that the shell process was terminated by a signal, allowing it to handle the termination appropriately. Specifically, if the parent process is also yash, the value of the special parameter ? in the child shell process is reproduced in the parent shell process without modification.

This signal-passing behavior is not supported by all shells; in shells that do not support it, the lower 8 bits of the exit status are passed to the parent process instead. The parent process is likely to interpret this as an ordinary exit status, which may not accurately reflect the original command’s termination by a signal.

The true and false utilities

The true and false utilities simply return an exit status of 0 and 1, respectively. They are often used as placeholders in conditional statements or loops. See the examples in the And-or lists section below.

Inverting exit status

You can invert a command’s exit status using the ! reserved word. This treats a successful command as a failure, and vice versa.

$ test -f /nonexistent/file
$ echo $?
1
$ ! test -f /nonexistent/file
$ echo $?
0

See Negation for more details.

And-or lists

An and-or list is a sequence of commands that are executed based on the success or failure of previous commands. It allows you to control the flow of execution based on the exit status of commands. An and-or list consists of commands separated by && (and) or || (or) operators. The && operator executes the next command only if the previous command succeeded (exit status 0), while the || operator executes the next command only if the previous command failed (non-zero exit status).

$ test -f /nonexistent/file && echo "File exists" || echo "File does not exist"
File does not exist

Unlike many other programming languages, the && and || operators have equal precedence with left associativity in the shell language:

$ false && echo foo || echo bar
bar
$ { false && echo foo; } || echo bar
bar
$ false && { echo foo || echo bar; }
$ true || echo foo && echo bar
bar
$ { true || echo foo; } && echo bar
bar
$ true || { echo foo && echo bar; }

The && and || operators can be followed by linebreaks for readability:

$ test -f /nonexistent/file &&
> echo "File exists" ||
> echo "File does not exist"
File does not exist

Line continuation can also be used to split and-or lists across multiple lines:

$ test -f /nonexistent/file \
> && echo "File exists" \
> || echo "File does not exist"
File does not exist

The exit status of an and-or list is the exit status of the last command executed in the list.

If commands

An if command is a conditional command that executes a block of commands based on the exit status of a test command. It allows you to perform different actions depending on whether a condition is true or false.

The minimal form of an if command uses the if, then, and fi reserved words that surround commands:

$ mkdir $$ && cd $$ && > foo.txt > bar.txt || exit
$ if diff -q foo.txt bar.txt; then echo "Files are identical"; fi
Files are identical

For readability, each reserved word can be on a separate line:

$ mkdir $$ && cd $$ && > foo.txt > bar.txt || exit
$ if diff -q foo.txt bar.txt
> then
>     echo "Files are identical"
> fi
Files are identical

You can also use the elif reserved word to add additional conditions:

$ if [ -f /dev/tty ]; then
>     echo "/dev/tty is a regular file"
> elif [ -d /dev/tty ]; then
>     echo "/dev/tty is a directory"
> elif [ -c /dev/tty ]; then
>     echo "/dev/tty is a character device"
> fi
/dev/tty is a character device

The else reserved word can be used to provide a default action if none of the conditions are met:

$ file=/nonexistent/file
$ if [ -e "$file" ]; then
>     echo "$file exists"
> elif [ -L "$file" ]; then
>     echo "$file is a symbolic link to a nonexistent file"
> else
>     echo "$file does not exist"
> fi
/nonexistent/file does not exist

The exit status of an if command is the exit status of the last command executed in the then or else clause. If no condition is met and there is no else clause, the exit status is 0 (success).

For repeating commands depending on a condition, see While and until loops.

Exiting on errors

By default, the shell continues running commands even if one fails (returns a non-zero exit status). This can cause later commands to run when they shouldn’t. If you enable the errexit shell option, the shell will exit immediately when any command fails, stopping further execution.

$ set -o errexit # or: set -e
$ test -e /dev/null
$ echo "Ok, continuing..."
Ok, continuing...
$ test -e /nonexistent/file
$ echo "This will not be printed"

In this example, after test -e /nonexistent/file fails, the shell exits right away, so you won’t see any more prompts or output.

The errexit option only applies to the result of pipelines. It is ignored in these cases:

Although errexit does not catch every error, it is recommended for scripts to avoid unexpected results from failed commands. To skip errexit for a specific command, append && true:

$ set -o errexit
$ test -e /nonexistent/file && true
$ echo "The exit status was $?"
The exit status was 1

Pattern-based branching

The case command performs pattern matching on a value and executes commands for the first matching pattern. This is useful for branching logic based on specific values or patterns.

Case command basics

A case command begins with the case reserved word, followed by the value to match. After the in reserved word, each branch specifies a pattern in parentheses, followed by a block of commands. Each block ends with ;;, and the command ends with esac.

For example, this command matches the value of foo and runs the corresponding commands:

$ case foo in
> (foo)
>     echo "Matched foo"
>     ;;
> (bar)
>     echo "Matched bar"
>     ;;
> esac
Matched foo

Patterns

Patterns can use wildcards and bracket expressions for flexible matching. For example, to match any string starting with f:

$ case foo in
> (f*)
>     echo "Starts with f"
>     ;;
> (b*)
>     echo "Starts with b"
>     ;;
> esac
Starts with f

To match multiple patterns, separate them with a pipe |:

$ case foo in
> (foo|bar)
>     echo "Matched foo or bar"
>     ;;
> esac
Matched foo or bar

Word expansion

Both the value and patterns undergo word expansion:

$ value="Hello" pattern="[Hh]*"
$ case $value in
> ($pattern)
>     echo "Matched pattern"
>     ;;
> esac
Matched pattern

The value is always expanded first. Patterns are expanded only when the shell needs to match them. Once a pattern matches, remaining patterns are not expanded.

Quote special characters in values or patterns to avoid unwanted expansion or matching:

$ case ? in
> ('?')
>     echo "Matched a single question mark"
>     ;;
> (?)
>     echo "Matched any single character"
>     ;;
> esac
Matched a single question mark

Continuing to the next branch

Instead of ;;, use ;& to continue execution with the next branch, regardless of whether its pattern matches. This allows multiple branches to run in sequence:

$ case foo in
> (foo)
>     echo "Matched foo"
>     ;&
> (bar)
>     echo "Matched bar, or continued from foo"
>     ;;
> (baz)
>     echo "Matched baz"
>     ;;
> esac
Matched foo
Matched bar, or continued from foo

Use ;;& or ;| to continue pattern matching in subsequent branches, so commands in multiple matching branches can run:

$ case foo in
> (foo)
>     echo "Matched foo"
>     ;;&
> (bar)
>     echo "Matched bar"
>     ;;
> (f*)
>     echo "Matched any string starting with f"
>     ;;
> esac
Matched foo
Matched any string starting with f

The ;;& and ;| terminators are extensions to POSIX. yash-rs supports both, but other shells may support only one or neither.

Miscellaneous

If no branch matches, or there are no branches, the shell skips the case command without error.

Use * as a catch-all pattern:

$ case foo in
> (bar)
>     echo "Matched bar"
>     ;;
> (*)
>     echo "Matched anything else"
>     ;;
> esac
Matched anything else

Use '' or "" as an empty value or pattern:

$ case "" in
> ('')
>     echo "Matched empty string"
>     ;;
> esac
Matched empty string

The opening parenthesis ( can be omitted if the first pattern is not literally esac, but parentheses are recommended for clarity:

$ case foo in
> foo)
>     echo "Matched foo"
>     ;;
> esac
Matched foo

Branches can have empty command blocks:

$ case bar in
> (foo)
>     ;;
> (bar)
>     echo "Matched bar"
>     ;;
> esac
Matched bar

The ;; terminator can be omitted for the last branch:

$ case foo in
> (foo)
>     echo "Matched foo"
>     ;;
> (bar)
>     echo "Matched bar"
> esac
Matched foo

Exit status

The exit status of case is that of the last command executed in the last executed branch. If the last executed branch has no commands, or no pattern matches, the exit status is 0.

Formal syntax

The formal syntax of the case command, in Extended Backus-Naur Form (EBNF):

case_command := "case", word, { newline }, "in", { newline },
                { branch }, [ last_branch ], "esac";
newline      := "\n";
branch       := pattern_list, branch_body, terminator, { newline };
last_branch  := pattern_list, branch_body;
pattern_list := "(", word, { "|" , word }, ")"
              | (word - "esac"), { "|" , word }, ")";
branch_body  := { newline }, [ list, [ newline, branch_body ] ];
terminator   := ";;" | ";&" | ";;&" | ";|";

Loops

Loops repeatedly execute a sequence of commands, either by iterating over a list or while a condition holds. They are useful for automating repetitive tasks or processing multiple items.

For loops

A for loop iterates over a list of strings, executing a block of commands for each string. In each iteration, the string is assigned to a variable for use in the commands.

$ for user in alice bob charlie; do
>     echo "Hello, $user!"
> done
Hello, alice!
Hello, bob!
Hello, charlie!

The word after for is the loop variable, assigned to each string in the list after in. The do reserved word starts the command block, and done ends the loop.

The semicolon after the list is optional if do is on a new line:

$ for user in alice bob charlie
> do
>     echo "Hello, $user!"
> done
Hello, alice!
Hello, bob!
Hello, charlie!

The in reserved word can also be on a separate line:

$ for user
> in alice bob charlie; do
>     echo "Hello, $user!"
> done
Hello, alice!
Hello, bob!
Hello, charlie!

Word expansion is performed on the list:

$ for file in *.txt; do
>     echo "$file contains $(wc -l -- "$file") lines"
>     echo "First line: $(head -n 1 -- "$file")"
> done
file1.txt contains 10 lines
First line: This is the first line of file1.
file2.txt contains 5 lines
First line: This is the first line of file2.

If the list is empty, the loop does not run:

$ for user in; do
>     echo "Hello, $user!"
> done

If in and the list are omitted, the loop iterates over the positional parameters as if in "$@" were specified:

$ set alice bob charlie
$ for user do
>     echo "Hello, $user!"
> done
Hello, alice!
Hello, bob!
Hello, charlie!

The exit status of a for loop is the exit status of the last command run in the loop, or 0 if the loop does not run.

While and until loops

A while loop executes commands as long as a condition is true. An until loop is similar, but continues until the condition becomes true. The do reserved word separates the condition from the loop body, and done ends the loop.

$ count=1
$ while [ $count -le 5 ]; do
>     echo "Count: $count"
>     count=$((count + 1))
> done
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
$ count=1
$ until [ $count -gt 3 ]; do
>     echo "Count: $count"
>     count=$((count + 1))
> done
Count: 1
Count: 2
Count: 3

See Exit status and conditionals for details on how exit status affects loop conditions.

The exit status of a while or until loop is that of the last command run in the loop body, or 0 if the loop body does not run. Note that the exit status of the condition does not affect the exit status of the loop.

Break and continue

The break utility exits the current loop. The continue utility skips to the next iteration.

$ for i in 1 2 3 4 5; do
>     if [ $i -eq 3 ]; then
>         echo "Breaking at $i"
>         break
>     fi
>     echo "Iteration $i"
> done
Iteration 1
Iteration 2
Breaking at 3
$ for i in 1 2 3 4 5; do
>     if [ $i -eq 3 ]; then
>         echo "Skipping $i"
>         continue
>     fi
>     echo "Iteration $i"
> done
Iteration 1
Iteration 2
Skipping 3
Iteration 4
Iteration 5

By default, break and continue affect the innermost loop. You can specify a numeric operand to affect the n’th outer loop:

$ for i in 1 2; do
>     for j in a b c; do
>         if [ "$j" = "b" ]; then
>             echo "Breaking outer loop at $i, $j"
>             break 2
>         fi
>         echo "Inner loop: $i, $j"
>     done
> done
Inner loop: 1, a
Breaking outer loop at 1, b

Lists and asynchronous commands

A list is a sequence of and-or lists separated by semicolons (;) or ampersands (&). Lists let you write multiple commands on a single line, and control whether they run synchronously or asynchronously.

Synchronous commands

When an and-or list is separated by a semicolon (;), it runs synchronously: the shell waits for the command to finish before running the next one.

$ echo "First command"; echo "Second command";
First command
Second command

The semicolon can be omitted after the last command:

$ echo "First command"; echo "Second command"
First command
Second command

Asynchronous commands

When an and-or list is separated by an ampersand (&), it runs asynchronously: the shell does not wait for the command to finish before running the next one.

$ echo "First async command" & echo "Second async command" & echo "Synchronous command"
Second async command
Synchronous command
First async command

Here, the commands run in parallel, so their output may appear in any order.

In an interactive shell, starting an asynchronous command prints its job number and process ID:

$ echo "Async command" &
[1] 12345
Async command

Because the shell does not wait for asynchronous commands, they may keep running while the shell reads new commands or even after the shell exits. To wait for them to finish, use the wait utility (see below).

Input redirection

By default, an asynchronous command’s standard input is redirected to /dev/null to prevent it from interfering with synchronous commands that read from standard input. This does not apply in job-controlling shells.

$ echo Input | {
>     cat &
>     read -r line
>     echo "Read line: $line"
> }
Read line: Input

In this example, the asynchronous cat reads from /dev/null, while read reads from standard input.

The ! special parameter

The ! special parameter gives the process ID of the last asynchronous command started in the shell. This is useful for tracking or waiting for background jobs.

The wait utility

The wait utility waits for asynchronous commands to finish. With no operands, it waits for all asynchronous commands started in the current shell. With operands, it waits for the specified process IDs.

$ mkdir $$ && cd $$ || exit
$ echo "Async command" > async.txt &
$ echo "Synchronous command"
Synchronous command
$ wait $!
$ cat async.txt
Async command

Here, the shell starts an asynchronous command that writes to a file. wait $! waits for it to finish before reading the file.

Job control

In yash-rs, all asynchronous commands start as background jobs. If the monitor shell option is enabled, you can use job control commands to manage these jobs. See the job control documentation for details.

Functions

A function is a named block of code you can call by name. Functions let you organize and reuse code in scripts and interactive sessions.

$ greet() {
>   echo "Hello, $1!"
> }
$ greet Alice
Hello, Alice!
$ greet Bob
Hello, Bob!

Defining functions

To define a function, write the function name followed by parentheses () and a compound command as the body:

$ greet() {
>   echo "Hello, $1!"
> }

You can also write the parentheses separately, and put the body on the next line:

$ cleanup ( )
> if [ -d /tmp/myapp ]; then
>   rm -rf /tmp/myapp
> fi

Function names are case-sensitive and do not share a namespace with variables.

By POSIX.1-2024, function names must use ASCII letters, digits, and underscores, and not start with a digit. As an extension, yash-rs allows any word as a function name. The function name is expanded when defined:

$ "$(echo foo)"() { echo "This function is named foo."; }
$ foo
This function is named foo.

A function is defined when the definition command is executed, not when parsed. For example, greet is only defined if the current year is 2001:

$ if [ "$(date +%Y)" = 2001 ]; then
>     greet() { echo "Happy millennium!"; }
> fi
$ greet
error: cannot execute external utility "greet"
 --> <stdin>:4:1
  |
4 | greet
  | ^^^^^ utility not found
  |

Redirections in a function definition apply when the function is called, not when it is defined:

$ dumb() { echo "Hello, $1!"; } > /dev/null
$ dumb Alice

You can redefine a function by defining it again with the same name. The new definition replaces the old one.

The exit status of a function definition is 0 if successful. It is nonzero if the function name expansion fails or if a readonly function with the same name exists.

Defining functions with the function reserved word is not POSIX and is not yet implemented in yash-rs.

Readonly functions

Make a function readonly with the typeset built-in. Readonly functions cannot be redefined or removed.

$ greet() { echo "Hello, World!"; }
$ typeset -fr greet
$ greet() { echo "Hello again!"; }
error: cannot redefine read-only function `greet`
 --> <stdin>:3:1
  |
3 | greet() { echo "Hello again!"; }
  | ^^^^^ failed function redefinition
  |
 ::: <stdin>:1:1
  |
1 | greet() { echo "Hello, World!"; }
  | ----- info: existing function was defined here
  |
 ::: <stdin>:2:13
  |
2 | typeset -fr greet
  |             ----- info: existing function was made read-only here
  |

The readonly built-in does not yet support making functions readonly in yash-rs.

Executing functions

To run a function, specify its name as a command name in a simple command.

$ greet() { echo "Hello, World!"; }
$ greet
Hello, World!

A function cannot be executed as a simple command if its name matches a special built-in or contains a slash. (See command search.)

Function parameters

Fields after the function name are passed as positional parameters. The original positional parameters are restored when the function returns.

$ foo() {
>     echo "The function received $# arguments, which are: $*"
> }
$ set alice bob charlie
$ echo "Positional parameters before calling foo: $*"
Positional parameters before calling foo: alice bob charlie
$ foo andrea barbie cindy
The function received 3 arguments, which are: andrea barbie cindy
$ echo "Positional parameters after calling foo: $*"
Positional parameters after calling foo: alice bob charlie

Returning from functions

A function runs until the end of its body or until the return built-in is called. return can exit the function early and set the exit status.

$ is_positive() {
>     if [ "$1" -le 0 ]; then
>         echo "$1 is not positive."
>         return 1
>     fi
>     echo "$1 is positive."
>     return
> }
$ is_positive 5
5 is positive.
$ echo "Exit status: $?"
Exit status: 0
$ is_positive -3
-3 is not positive.
$ echo "Exit status: $?"
Exit status: 1

Removing functions

Remove a function with the unset built-in and the -f option:

$ greet() { echo "Hello, World!"; }
$ unset -f greet
$ greet
error: cannot execute external utility "greet"
 --> <stdin>:3:1
  |
3 | greet
  | ^^^^^ utility not found
  |

Replacing existing utilities

You can override existing utilities (except special built-ins) by defining a function with the same name. This is useful for customizing or extending utility behavior. To run the original utility from within your function, use the command built-in:

$ ls() {
>     command ls --color=auto "$@"
> }
$ ls
Documents  Downloads  Music  Pictures  Videos

See Local variables for temporary variables that are removed when the function returns.

See Aliases and functions for comparison between aliases and functions.

Aliases

Alias substitution replaces part of a command with a predefined string while parsing the command. Aliases are useful for creating shortcuts or customizing command behavior.

Basic usage

Define an alias with the alias built-in. When the first word in a simple command matches an alias, the shell replaces it with the alias definition before parsing the rest of the command.

$ alias ll='ls -l'
$ ll
total 40
drwxr-xr-x 6 alice users  4096 Jun 21 12:57 book
-rw-r--r-- 1 alice users   397 May 14 21:57 book.toml
-rwxr-xr-x 1 alice users  4801 Jun 16 22:22 doctest.sh
-rw-r--r-- 1 alice users 20138 May 28 00:04 LICENSE
drwxr-xr-x 3 alice users  4096 May 31 02:11 src

Aliases can include multiple words, redirections, and delimiters. They can reference other aliases, which are expanded recursively.

$ alias dumb='> /dev/null'
$ dumb echo "Hello, World!"

Here, the second line becomes > /dev/null echo "Hello, World!", so nothing is printed.

$ alias 2001='test "$(date +%Y)" = 2001 &&'
$ 2001 echo "Happy millennium!"

This expands to test "$(date +%Y)" = 2001 && echo "Happy millennium!", printing the message if the year is 2001.

Alias names

By POSIX.1-2024, alias names can use ASCII letters, digits, and !, %, ,, -, @, _. Yash-rs allows any literal word as an alias name (no quotes or expansions). Alias names are case-sensitive.

Recursion

Aliases can reference other aliases, creating a chain of substitutions. The shell expands aliases recursively until no more aliases are found. An alias is not substituted in the result of its own expansion, preventing infinite loops.

$ alias ll='ls -l'
$ alias l='ll -h'
$ l
total 40K
drwxr-xr-x 6 alice users 4.0K Jun 22 11:36 book
-rw-r--r-- 1 alice users  397 May 14 21:57 book.toml
-rwxr-xr-x 1 alice users 4.7K Jun 16 22:22 doctest.sh
-rw-r--r-- 1 alice users  20K May 28 00:04 LICENSE
drwxr-xr-x 3 alice users 4.0K May 31 02:11 src
$ alias ls='ls -F'
$ ls
book/  book.toml  doctest.sh*  LICENSE  src/

Continued substitution

If an alias definition ends with a blank, the next word is also checked for alias substitution, even if it is not the first word of the command. This is useful for utilities that take another command as an argument.

$ alias greet='echo Hello,'
$ alias time='time -p '
$ time greet World
Hello, World
real 0.00
user 0.00
sys 0.00

If the time alias does not end with a blank, the next word is not substituted:

$ alias greet='echo Hello,'
$ alias time='time -p'
$ time greet World
time: cannot run greet: No such file or directory
real 0.01
user 0.00
sys 0.00

Note: In yash and many other shells, this behavior only applies if the next word is a whole word, not a part of a word. In the following example, a follows a blank resulting from alias substitution for q, but it is inside quotes, so it is not substituted:

$ alias echo='echo ' q="'[ " a=b
$ echo q a ]'
[  a ]

Miscellaneous

To prevent alias substitution for a word, quote it.

Aliases become effective after the defining command is executed. Since commands are parsed and executed line by line, aliases defined in the current line are not available in the same line.

Remove an alias with the unalias built-in.

Aliases and functions

Functions are similar to aliases in that both let you define names for command sequences. Functions are better for complex logic—they can take parameters, use local variables, and include conditionals, loops, and other compound commands. Aliases are better for syntactic manipulation, such as inserting a pipeline or redirection, because they are expanded as the command is parsed.

Redirections

Redirections control where command input and output go. They let you save output to files, read input from files, or otherwise manipulate how commands perform I/O operations.

What are file descriptors?

A file descriptor is a non-negative integer that identifies an open file or I/O channel in a process. When a process opens a file, the operating system assigns it a file descriptor, which the process uses to read from or write to that file.

The first three file descriptors have standard meanings:

  • 0: Standard input – the source of input data
  • 1: Standard output – the destination for command results
  • 2: Standard error – the destination for error messages and diagnostics

By default, these are connected to the terminal, but they can be redirected to files or other destinations.

Redirection syntax

A redirection consists of a special operator followed by a target (such as a file or file descriptor). Redirections can appear anywhere in a simple command, or after the body of a compound command.

For example, the > operator redirects standard output to a file:

$ mkdir $$ && cd $$ || exit
$ echo "Hello, World!" > output.txt
$ cat output.txt
Hello, World!

The < operator redirects standard input from a file:

$ mkdir $$ && cd $$ || exit
$ printf 'One\nTwo\nThree\n' > input.txt
$ while read -r line; do
>     echo "Read: $line"
> done < input.txt
Read: One
Read: Two
Read: Three

Redirection operators

Yash-rs supports these redirection operators:

  • < (file): Redirects standard input from a file.

  • > (file): Redirects standard output to a file.

    • If the clobber shell option is set (default), > behaves like >|.
    • If clobber is not set, > fails if the file exists and is a regular file or a symlink to a non-existent file. Otherwise, it creates a new file or opens the existing one. This is useful for preventing accidental overwriting of files.
    $ mkdir $$ && cd $$ || exit
    $ set -o noclobber
    $ echo "Hello, World!" > file.txt
    $ cat file.txt
    Hello, World!
    $ echo "Another redirection" > file.txt
    error: cannot open the file
     --> <stdin>:5:30
      |
    5 | echo "Another redirection" > file.txt
      |                              ^^^^^^^^ file.txt: File exists (os error 17)
      |
    
  • >| (file): Redirects standard output to a file, overwriting it if it exists.

    • Always overwrites existing files, regardless of the clobber option.
    • Truncates the file if it exists, or creates it if not.
    $ mkdir $$ && cd $$ || exit
    $ echo "Hello, World!" >| file.txt
    $ cat file.txt
    Hello, World!
    $ echo "Another redirection" >| file.txt
    $ cat file.txt
    Another redirection
    
  • >> (file): Redirects standard output to a file, appending to it if it exists.

    • Appends to the file if it exists, or creates it if not.
    $ mkdir $$ && cd $$ || exit
    $ echo "Hello, World!" >> file.txt
    $ cat file.txt
    Hello, World!
    $ echo "Another line" >> file.txt
    $ cat file.txt
    Hello, World!
    Another line
    
  • <> (file): Opens a file for both reading and writing.

    • Opens the file if it exists, or creates it if not.
  • <&: Duplicates or closes standard input, depending on the target word:

    • If the word is a file descriptor number, standard input becomes a copy of that descriptor (which must be open for reading).
    • If the word is -, standard input is closed. No error is reported if it is already closed.
      • If standard input is closed, commands that read from it will fail. To provide empty input, use < /dev/null instead.
  • >&: Duplicates or closes standard output, depending on the target word:

    • If the word is a file descriptor number, standard output becomes a copy of that descriptor (which must be open for writing).

    • If the word is -, standard output is closed. No error is reported if it is already closed.

      • If standard output is closed, commands that write to it will fail. To discard output, use > /dev/null instead.
    • For example, >&2 redirects standard output to standard error:

      $ echo "error: please specify a user" >&2
      error: please specify a user
      

Specifying file descriptors

Redirection operators starting with < default to standard input; those starting with > default to standard output. To redirect a different descriptor, prefix the operator with its number (no space):

For example, to redirect standard error to a file:

$ grep "pattern" input.txt 2> error.log

If you insert a space, the number is treated as a command argument, not a file descriptor:

$ mkdir $$ && cd $$ || exit
$ echo 2 > output.txt
$ cat output.txt
2

Some shells allow using a variable name in braces {} instead of a file descriptor. For file-opening redirections, the shell allocates a new descriptor and assigns it to the variable. For descriptor-copying redirections, the shell uses the descriptor stored in the variable.

This is not yet implemented in yash-rs, but would look like:

$ exec {fd}> output.txt
$ echo "Hello, World!" >&$fd
$ cat output.txt
Hello, World!

Target word expansion

Except for here-documents, the word after a redirection operator is expanded before use. The following expansions are performed:

Pathname expansion may be supported in the future.

$ mkdir $$ && cd $$ || exit
$ file="output.txt"
$ echo "Hello, World!" > "$file"
$ cat "$file"
Hello, World!

Persistent redirections

By default, redirections only apply to the command they are attached to. To make a redirection persist across multiple commands, use the exec built-in without arguments:

$ mkdir $$ && cd $$ || exit
$ exec > output.txt
$ echo "Hello, World!"
$ echo "More greetings!"
$ cat output.txt >&2
Hello, World!
More greetings!

You can use the >& operator to save a file descriptor before redirecting it, and restore it later:

$ mkdir $$ && cd $$ || exit
$ exec 3>&1 # Save current standard output to file descriptor 3
$ exec > output.txt # Redirect standard output to a file
$ echo "Hello, World!"
$ exec >&3 # Restore standard output from file descriptor 3
$ exec 3>&- # Close file descriptor 3
$ echo "More greetings!"
More greetings!
$ cat output.txt
Hello, World!

Semantic details

Applying a redirection to a compound command is different from applying it to a simple command inside the compound command. Each use of < opens a new file descriptor at the start of the file. If the redirection is inside a loop, the file descriptor is reset to the beginning on each iteration:

$ printf 'One\nTwo\nThree\n' > input.txt
$ while read -r line < input.txt; do
>     echo "Read: $line"
> done
Read: One
Read: One
Read: One
Read: One
Read: One
(The loop never ends…)

If a command has multiple redirections, they are applied in order. If several affect the same file descriptor, the last one takes effect:

$ mkdir $$ && cd $$ || exit
$ echo "Hello, World!" > dummy.txt > output.txt 2> error.txt
$ cat dummy.txt
$ cat output.txt
Hello, World!
$ cat error.txt

Note the difference between > /dev/null 2>&1 and 2>&1 > /dev/null:

  • > /dev/null 2>&1 redirects both standard output and standard error to /dev/null, discarding both.
  • 2>&1 > /dev/null redirects standard error to standard output, and then redirects standard output to /dev/null. This means standard error is still printed to the terminal (or wherever standard output was originally directed).
$ cat /nonexistent/file > /dev/null 2>&1
$ cat /nonexistent/file 2>&1 > /dev/null
cat: /nonexistent/file: No such file or directory

Here-documents

Here-documents are a type of redirection that lets you provide multi-line input directly within a script or command line. They are useful for supplying input to commands or scripts without creating a separate file.

Syntax

A here-document starts with the << operator followed by a delimiter word. After the next newline operator, the shell reads lines until it finds a line containing only the delimiter (with no trailing blanks). The lines read become the standard input for the command.

$ cat <<EOF
> Hello,
> World!
> EOF
Hello,
World!

In this example, EOF is the delimiter. The cat utility receives the lines between <<EOF and the final EOF.

POSIX allows any word as a delimiter, but for portability, use only alphanumeric characters and underscores. Delimiters with special characters or whitespace can cause unexpected behavior, especially if not quoted. See also Quoting the delimiter and expanding the content below for the effects of quoting the delimiter.

Multiple here-documents

You can use multiple here-document operators in a single command or across multiple commands on the same line. After the next newline, the shell reads lines for each here-document in order, stopping at each delimiter.

$ cat <<EOF; cat <<END <<EOF
> Hello,
> EOF
> This is the first here-document for the second command.
> END
> World!
> EOF
Hello,
World!

Here-documents in command substitution

When using a here-document inside command substitution, the content must be included within the substitution syntax:

$ echo $(cat <<EOF
> Hello,
> World!
> EOF
> )
Hello, World!

It is not supported to place the here-document content outside the command substitution, as in:

echo $(cat <<EOF)
Hello,
World!
EOF

Automatic removal of leading tabs

If you use <<- instead of <<, all leading tab characters are removed from the here-document content and the delimiter line. This allows you to indent here-documents in your scripts for readability, without affecting the output.

$ cat <<-EOF
> 		Hello,
> 		World!
> 	EOF
Hello,
World!

Note: Only leading tabs are removed, not spaces.

Quoting the delimiter and expanding the content

If the delimiter after the redirection operator is quoted, quote removal is performed on the delimiter, and the result is used to find the end of the here-document. In this case, the content is not subject to any expansions and is treated literally.

$ user="Alice"
$ cat <<'EOF'
> Hello, $user!
> 1 + 1 = $((1 + 1)).
> EOF
Hello, $user!
1 + 1 = $((1 + 1)).

If the delimiter is not quoted, the following are handled in the here-document content when the redirection is performed:

Single and double quotes in the here-document content are treated literally.

$ user="Alice"
$ cat <<EOF
> Hello, $user!
> 1 + 1 = $((1 + 1)).
> EOF
Hello, Alice!
1 + 1 = 2.

Shell environment and subshells

The shell execution environment is the set of state the shell maintains to control its behavior. It consists of:

Subshells

A subshell is a separate environment created as a copy of the current shell environment. Changes in a subshell do not affect the parent shell. A subshell starts with the same state as the parent, except that traps with custom commands are reset to default behavior.

Create a subshell using parentheses. Subshells are also created implicitly when running an external utility, a command substitution, an asynchronous command, or a multi-command pipeline.

Subshells of an interactive shell are not themselves interactive, even if the interactive option is set.

Yash-rs currently implements subshells using the fork system call, which creates a new process. This may change in the future for greater efficiency.

External utilities run by the shell inherit the following from the shell environment:

  • File descriptors
  • Working directory
  • File creation mask
  • Resource limits
  • Environment variables
  • Traps, except those with a custom command

Shell options

Shell options control the behavior of the shell. You can enable (set) or disable (unset) them using command line arguments at startup or with the set built-in during a shell session.

Enabling and disabling options

You can specify shell options as command line arguments when starting the shell, or with the set built-in. In yash, all options have a long name, and some also have a short name.

Options set at startup take effect before the shell reads and executes commands. Options set with set affect the current shell session. Some options are only available at startup; others can be changed at any time. The syntax is the same in both cases.

Long option names

Long options start with --. For example, to enable the allexport option at startup:

yash3 --allexport

You can also specify long options with the -o option:

yash3 -o allexport

Only alphanumeric characters matter in long option names, and they are case-insensitive. For example, --all-export, --ALLEXPORT, and ---All*Ex!PorT all enable allexport.

Long option names can be abbreviated if unambiguous. For example, --cl enables clobber:

$ set --cl
$ set --c
error: ambiguous option name "--c"
 --> <stdin>:2:5
  |
2 | set --c
  | --- ^^^ --c
  | |
  | info: executing the set built-in
  |

Note: Future versions may add more options, so abbreviations that work now may become ambiguous later. For forward compatibility, use full option names.

To disable a long option, prepend no to the name:

yash3 --noallexport

Or use ++ instead of --:

yash3 ++allexport

Or use +o instead of -o:

yash3 +o allexport

If you use both + and no, it is a double negation and enables the option:

yash3 +o noallexport

Short option names

Some options have short names, specified as a single character. For example, to enable allexport with its short name:

yash3 -a

To disable it:

yash3 +a

You can combine multiple short options in one argument:

yash3 -aex

Some short options negate long options. For example, -C is the same as --noclobber (disables clobber). To enable clobber with its short name, use +C.

Viewing current options

To see current shell options, use set -o with no arguments:

$ set -o
allexport        off
clobber          on
cmdline          off
errexit          off
exec             on
glob             on
hashondefinition off
ignoreeof        off
interactive      off
log              on
login            off
monitor          off
notify           off
posixlycorrect   off
stdin            on
unset            on
verbose          off
vi               off
xtrace           off

set +o prints options in a format that can be used to restore them:

$ set +o
set +o allexport
set -o clobber
#set +o cmdline
set +o errexit
set -o exec
set -o glob
set +o hashondefinition
set +o ignoreeof
#set +o interactive
set -o log
set +o login
set +o monitor
set +o notify
set +o posixlycorrect
#set -o stdin
set -o unset
set +o verbose
set +o vi
set +o xtrace
$ set +o allexport
$ savedoptions=$(set +o)
$ set -o allexport
$ eval "$savedoptions"
$ set -o | grep allexport
allexport        off

The - special parameter contains the currently set short options. For example, if -i and -m are set, the value of - is im. Options without a short name are not included. Short options that negate long options are included when the long option is unset.

$ set -a -o noclobber
$ echo "$-"
aCs

Option list

Below is a list of all shell options in yash-rs, with their long and short names, and a brief description. Unless noted, all options are disabled by default.

  • allexport (-a): If set, all variables assigned in the shell are exported.

  • clobber (+C): If set (default), the > redirection operator overwrites existing files. If unset, > fails if the file exists. The >| operator always overwrites files.

  • cmdline (-c): If set, the shell executes the first operand from the command line as a command. Mutually exclusive with stdin, and only settable at startup.

  • errexit (-e): If set, the shell exits if a command fails. Useful for scripts to stop on errors. See Exiting on errors for details.

  • exec (+n): If set (default), the shell executes commands. If unset, it only parses commands (useful for syntax checking).

    • Once unset, it cannot be set again in the same session.
    • In interactive shells, this option is ignored and commands are always executed.
  • glob (+f): If set (default), the shell performs pathname expansion on words containing metacharacters. If unset, pathname expansion is skipped.

  • hashondefinition (-h): Deprecated and has no effect. Remains for compatibility.

    • The short name -h is currently a synonym for --hashondefinition, but this may change.
    • Many shells implement -h differently, so behavior may vary.
  • ignoreeof: If set, the shell ignores end-of-file (usually Ctrl+D) and does not exit. See Preventing accidental exits.

    • Only takes effect if the shell is interactive and input is a terminal.
  • interactive (-i): If set, the shell is interactive.

    • Enabled on startup if stdin is enabled and standard input and error are terminals.
  • log: Deprecated and has no effect. Remains for compatibility.

  • login (-l): If set, the shell behaves as a login shell. Only settable at startup.

    • ⚠️ Currently has no effect in yash-rs. In the future, login shells will read extra initialization files.
  • monitor (-m): If set, the shell performs job control (allows managing background and foreground jobs).

  • notify (-b): If set, the shell notifies you of background job completions and suspensions as soon as they occur. If unset, notifications are delayed until the next prompt.

    • ⚠️ Currently has no effect in yash-rs. In the future, it will enable immediate notifications for background jobs.
    • Only takes effect if interactive and monitor are enabled.
  • pipefail: If set, the shell returns the exit status of the last command in a pipeline that failed, instead of the last command’s exit status. See Catching errors across pipeline components for details.

    • ⚠️ Not yet implemented in yash-rs.
  • posixlycorrect: If set, the shell behaves as POSIX-compliant as possible. Useful for portable scripts.

    • Enabled on startup if the shell is started as sh.
    • When unset, yash-rs may deviate from POSIX in some areas.
  • stdin (-s): If set, the shell reads commands from standard input. Mutually exclusive with cmdline, and only settable at startup.

    • Enabled if cmdline is not set and the shell is started with no operands.
  • unset (+u): If set (default), the shell expands unset variables to an empty string. If unset, expanding an unset variable raises an error. See Unset parameters (in parameter expansion) and Variables (in arithmetic expression) for details.

  • verbose (-v): If set, the shell prints each command before executing it. See Reviewing command input for details.

  • vi: If set, the shell uses vi-style keybindings for command line editing.

    • ⚠️ Currently has no effect in yash-rs. In the future, it will enable vi-style editing in interactive shells.
  • xtrace (-x): If set, the shell prints each field after expansion, before executing it. See Tracing command execution for details.

Compatibility

The syntax and options specified in POSIX.1-2024 are much more limited than those in yash-rs. For portable scripts, use only POSIX-specified syntax and options.

POSIX.1-2024 syntax:

  • Enable a long option: set -o optionname (no -- prefix).
  • Disable a long option: set +o optionname (no ++ prefix).
  • Long options are case-sensitive, must be spelled out in full, and cannot contain extra symbols.
  • No support for no-prefix inversion of long options.
  • Enable a short option: - followed by the option character.
  • Disable a short option: + followed by the option character.
  • Short options can be combined after the - or + prefix.
  • View current options: set -o or set +o.

POSIX.1-2024 options:

  • -a, -o allexport
  • -b, -o notify
  • -C, -o noclobber
  • -c
  • -e, -o errexit
  • -f, -o noglob
  • -h
  • -i
  • -m, -o monitor
  • -n, -o noexec
  • -s
  • -u, -o nounset
  • -v, -o verbose
  • -x, -o xtrace
  • -o ignoreeof
  • -o nolog
  • -o pipefail
  • -o vi

Signals and traps

Signals are a method of inter-process communication used to notify a process that a specific event has occurred. The POSIX standard defines a set of signals that have well-defined meanings and behaviors across Unix-like systems. Traps are the shell’s mechanism for handling signals and other events by executing custom commands when specific conditions occur.

What are signals?

Signals are asynchronous notifications sent to a process to inform it of an event. Common signals include:

  • SIGINT: Interrupt signal, typically sent when the user presses Ctrl+C
  • SIGTERM: Termination request, used to ask a process to exit gracefully
  • SIGQUIT: Quit signal, typically sent when the user presses Ctrl+\
  • SIGHUP: Hangup signal, originally sent when a terminal connection was lost
  • SIGKILL: Kill signal that cannot be caught or ignored
  • SIGSTOP: Stop signal that cannot be caught or ignored
  • SIGTSTP: Terminal stop signal, typically sent when the user presses Ctrl+Z
  • SIGCHLD: Child process terminated or stopped
  • SIGUSR1 and SIGUSR2: User-defined signals for custom applications

Available signals may vary by system. For a complete list, refer to your system’s documentation or use kill -l.

When a process receives a signal, it can respond in one of three ways:

  1. Default action: Follow the system’s default behavior for that signal (usually termination)
  2. Ignore: Do nothing when the signal is received
  3. Custom action: Execute a custom signal handler

What are traps?

Traps are the shell’s way of defining custom responses to signals and other events. When you set a trap, you specify:

  • A condition that triggers the trap (such as a signal or shell exit), and
  • An action to perform when the condition occurs.

The shell checks for pending signals and executes corresponding trap actions at safe points during execution, typically before and after executing commands. This ensures that trap actions run in a consistent shell state.

Special conditions

In addition to signals, the shell supports the EXIT condition, which is triggered when the shell exits (but not when killed by a signal). This allows you to run cleanup commands or perform other actions when the shell session ends.

More conditions may be supported in future versions of the shell.

Trap inheritance and subshells

When the shell creates a subshell:

  • Traps set to ignore are inherited by the subshell.
  • Traps with custom actions are reset to default behavior.

External utilities inherit the signal dispositions from the shell, but not custom trap actions.

Setting traps

Use the trap built-in to configure traps or view current traps.

Restrictions

  • SIGKILL and SIGSTOP cannot be caught or ignored.
  • If a non-interactive shell inherited an ignored signal, that signal cannot be trapped. Interactive shells can modify signals that were initially ignored.

How and when traps are executed

Signal traps run when signals are caught.

  • When a signal is caught while the shell is running a command, the shell waits for the command to finish before executing the trap action.
  • If a signal is caught while the shell is reading input, the shell waits for the input to complete before executing the trap action. This behavior may change in future versions so that traps can run immediately.
  • While executing a signal trap action, other signal traps are not processed (no reentrance), except in subshells.

EXIT traps run when the shell exits normally, after all other commands complete.

The exit status is preserved across trap action execution, but trap actions can use the exit built-in to terminate the shell with a specific exit status.

Auto-ignored signals

In an interactive shell, certain signals are automatically ignored by default to prevent the shell from being terminated or stopped unintentionally. Specifically:

  • SIGINT, SIGTERM, and SIGQUIT are always ignored.
  • If job control is enabled, SIGTSTP, SIGTTIN, and SIGTTOU are also ignored.

This ensures the shell remains responsive and in control, even if these signals are sent. You can still set traps for these signals if needed. In subshells, which are non-interactive, this automatic ignoring does not apply.

Startup

This section describes how yash-rs is started and configured.

Command-line arguments

Start the shell by running the yash3 executable. The general syntax is:

yash3 [options] [file [arguments…]]
yash3 [options] -c command [command_name [arguments…]]
yash3 [options] -s [arguments…]

The shell’s behavior is determined by the options and operands you provide.

Options

The shell accepts shell options to control its behavior. The following options are only available at startup:

  • -c (--cmdline): Read and execute commands from the command operand.
  • -s (--stdin): Read and execute commands from standard input.
  • -i (--interactive): Force the shell to be interactive.
  • -l (--login): Make the shell a login shell. This can also be triggered by a leading hyphen in the command name (e.g., -yash3).
  • --profile <file>: Specify a profile file to execute.
  • --noprofile: Do not execute any profile file.
  • --rcfile <file>: Specify an rcfile to execute.
  • --norcfile: Do not execute any rcfile.

Modes of operation

The shell has three modes:

  • File mode: If neither -c nor -s is specified, the first operand is treated as the path to a script file to execute. Any following operands become positional parameters for the script.
  • Command string mode: With -c, the shell executes the command string given as the first operand. If command_name is specified, it sets the special parameter 0. Remaining operands become positional parameters.
  • Standard input mode: With -s, the shell reads commands from standard input. Any operands are set as positional parameters.

If no operands are given and -c is not specified, the shell assumes -s.

Initialization files

When the shell starts, it may execute one or more initialization files to configure the environment.

Login shell

If the shell is a login shell (started with -l or a leading hyphen in its name), it executes a profile file. The path can be set with --profile. Use --noprofile to skip the profile file.

⚠️ Profile file execution is not yet implemented.

Interactive shell

If the shell is interactive, it executes an rcfile. The path can be set with --rcfile. Use --norcfile to skip the rcfile.

If no rcfile is specified, the shell checks the ENV environment variable. If set, its value is expanded for parameter expansion, command substitution, and arithmetic expansion, and used as the rcfile path.

The rcfile is only executed if:

  • The shell is interactive.
  • The real user ID matches the effective user ID.
  • The real group ID matches the effective group ID.

Compatibility

Options for initialization files (--profile, --noprofile, --rcfile, --norcfile) are not part of POSIX.1-2024 and may not be available in other shells. See Compatibility in the options documentation for portable shell options.

POSIX.1-2024 does not specify login shells or profile files. The behavior described here is specific to yash-rs and may differ from other shells.

Using the ENV environment variable for initialization files is POSIX-specified. In the future, yash-rs may support a different default rcfile location depending on a shell option.

Termination

A shell session terminates in the following cases:

  • When the shell reaches the end of input.
  • When you use the exit built-in.
  • When the shell receives a signal that causes it to terminate, such as SIGINT or SIGTERM, and no trap is set to handle that signal.
  • When a non-interactive shell is interrupted by a shell error.
  • When a command fails and the errexit option is enabled.

Preventing accidental exits

When the input to the shell is a terminal, you can signal an end-of-file with the eof sequence (usually Ctrl+D). However, you might not want the shell to exit immediately when this happens, especially if you often hit the sequence by mistake. Enable the ignoreeof shell option to prevent the shell from exiting on end-of-file and let it wait for more input.

$ set -o ignoreeof
$ 
# Type `exit` to leave the shell when the ignore-eof option is on.
$ exit

This option is only effective in interactive shells and only when the input is a terminal. As an escape, entering 50 eof sequences in a row will still cause the shell to exit, regardless of the ignoreeof option.

Exiting subshells

When one of the above conditions occurs in a subshell, the subshell exits. It does not directly cause the parent shell to exit, but the exit status of the subshell may affect the parent shell’s behavior, conditionally causing it to exit if the errexit option is set.

EXIT trap

You can set a trap for the EXIT condition to run commands when the shell exits. This can be useful for cleanup tasks or logging. The trap is executed regardless of how the shell exits, whether due to an error, end-of-file, or explicit exit command, except when the shell is killed by a signal, in which case the trap is not executed.

$ trap 'rm -f temporary.txt; echo "Temporary file removed."' EXIT
$ echo "Some data" > temporary.txt
$ cat temporary.txt
Some data
$ exit
Temporary file removed.

The EXIT trap is run at most once per shell session. Modifying the EXIT trap while it is running does not have any effect on trap execution.

Exit status

If the shell exits due to end of input, the exit built-in, or the errexit option, it returns the exit status of the last command executed. See Exit status of the shell for details.

If the shell exits because of a shell error, the exit status is a non-zero value indicating the error.

Shell errors

The following shell errors set the exit status to a non-zero value and may cause the shell to exit, depending on the situation:

  • Unrecoverable errors reading input
    • The shell exits immediately.
    • This does not apply to scripts read by the source built-in.
  • Command syntax errors
    • The shell exits if non-interactive.
    • If interactive, the shell ignores the current command and resumes reading input.
  • Errors in special built-in utilities
    • The shell exits if non-interactive or if the errexit option is set. Otherwise, it aborts the current command and resumes reading input.
    • This includes redirection errors for special built-ins.
    • This does not apply to special built-ins run via the command built-in.
  • Variable assignment errors and expansion errors
    • The shell exits if non-interactive or if errexit is set. Otherwise, it aborts the current command and resumes reading input.
  • Redirection errors (except for special built-ins)
    • The shell exits if errexit is set. Otherwise, it continues with the next command.

POSIX.1-2024 allows shells to exit on command search errors, but many shells, including yash-rs, do not.

Dynamic command evaluation

Two built-in utilities support dynamic command evaluation.

Evaluating command strings

The eval built-in evaluates a command string. This is useful for constructing and executing commands dynamically.

For example, you can use eval to assign a value to a variable whose name is chosen at runtime:

echo "Type a variable name:"
read -r varname
eval "$varname='Hello, world!'"
eval "echo 'The value of $varname is:' \$$varname"

Reading and executing files

The . (dot) built-in reads and executes commands from a file. This is useful for organizing scripts and reusing code.

For example, you can use . to source a file containing variable definitions:

# contents of vars.sh
greeting="Hello, world!"
farewell="Goodbye, world!"

# main script
. ./vars.sh
echo "$greeting"
echo "$farewell"

source is a non-POSIX synonym for the . built-in.

Script debugging

Yash-rs offers several features to help debug scripts.

Exiting on errors

Many utilities return a non-zero exit status when they fail, but by default the shell continues executing the next command, which can lead to unexpected results. To stop the script when a command fails, enable the errexit option. For details, see Exiting on errors.

Catching errors across pipeline components

By default, the exit status of a pipeline reflects only the last command, ignoring failures in earlier commands. To make the pipeline fail if any command fails, enable the pipefail shell option. With pipefail, the pipeline’s exit status is that of the last command that returned a non-zero status, or zero if all returned zero. This helps catch errors in pipelines.

The `pipefail` option is not yet implemented in yash-rs.

Blocking unset parameters

Unset parameters expand to an empty string by default, which can silently hide misspelled parameter names, potentially leading to unexpected results if the intended value is unused. To catch such errors early, enable the nounset shell option. With nounset, the shell raises an error whenever an unset parameter is expanded. See Unset parameters for more information.

This option also detects unset variables in arithmetic expressions.

Reviewing command input

When the verbose shell option is enabled, the shell prints each command to standard error as it reads it, before executing. This is useful for reviewing commands being executed, especially in scripts.

$ set -o verbose
$ echo "Hello, world!"
echo "Hello, world!"
Hello, world!
$ set -o verbose
$ greet() {
greet() {
> echo "Hello, world!"
echo "Hello, world!"
> }
}
$ greet
greet
Hello, world!

Tracing command execution

If you enable the xtrace shell option, the shell prints expanded fields in each command to standard error before executing it. This is useful for reviewing actual commands being executed.

$ mkdir $$ && cd $$
$ set -o xtrace
$ for user in Alice Bob Charlie; do
>     echo "Hello, $user!" >> greetings.txt
> done
+ for user in Alice Bob Charlie
+ echo 'Hello, Alice!' 1>>greetings.txt
+ echo 'Hello, Bob!' 1>>greetings.txt
+ echo 'Hello, Charlie!' 1>>greetings.txt
$ cat *.txt
+ cat greetings.txt
Hello, Alice!
Hello, Bob!
Hello, Charlie!

Each line of output is prefixed with the value of the PS4 variable, which defaults to + . Parameter expansion, command substitution, and arithmetic expansion are performed on the PS4 value before printing it.

$ PS4='$((i=i+1))+ '; set -o xtrace
$ while getopts n option -n foo; do
>     case $option in
>         (n) n_option=true ;;
>         (*) echo "Unknown option: $option" ;;
>     esac
> done
1+ getopts n option -n foo
2+ case n in
3+ n_option=true
4+ getopts n option -n foo

Checking syntax

If the exec shell option is unset, the shell only parses commands without executing them. This is useful for checking syntax errors in scripts without running them.

$ set +o exec
$ echo "Hello, world!"
$ echo "Oops, a syntax error";;
error: the compound command delimiter is unmatched
 --> <stdin>:3:28
  |
3 | echo "Oops, a syntax error";;
  |                            ^^ not in a `case` command
  |
# Invoke the shell with the `exec` option unset to check a script file
yash3 +o exec my_script.sh

Interactive shell

When the interactive shell option is enabled, the shell behaves in a way that is more suitable for interactive use. Currently, only the essential features are implemented, but more will be added in the future.

Enabling interactive mode

When you start the shell without arguments in a terminal, it usually enables interactive mode by default:

yash3

Specifically, interactive mode is enabled if:

To force the shell to be interactive, use the -i option:

yash3 -i

Interactive mode can only be set at startup. To change the interactive mode, you must restart the shell.

Telling if the shell is interactive

To determine if the shell is running in interactive mode, check whether the - special parameter contains i:

case $- in
  *i*) echo "Interactive shell" ;;
  *)   echo "Non-interactive shell" ;;
esac

See Viewing current options for additional methods.

What happens in interactive mode

When the shell is interactive:

Command prompt

When an interactive shell reads input, it displays a command prompt—a string indicating that the shell is ready to accept commands. The prompt can be customized to display information such as the current working directory, username, or hostname.

Customizing the command prompt

The command prompt is controlled by the PS1 and PS2 variables:

  • PS1 defines the primary prompt, shown when the shell is ready for a new command.
  • PS2 defines the secondary prompt, shown when the shell expects more input to complete a command (i.e., when a command spans multiple lines).

Each time the shell displays a prompt, it performs parameter expansion, command substitution, and arithmetic expansion on the prompt strings. This allows prompts to include dynamic information, such as the working directory or username.

After these expansions, the shell performs exclamation mark expansion (see below) on the PS1 prompt. PS2 is not subject to exclamation mark expansion.

The default values for these variables are:

PS1='$ '
PS2='> '

Many shells change the default PS1 to # for the root user, but yash-rs does not yet support this.

Custom prompts are usually set in the rcfile. For example, to include the username, hostname, and working directory in the prompt, add this to your rcfile:

PS1='${LOGNAME}@${HOSTNAME}:${PWD} $ '

You do not need to export PS1 or PS2 for them to take effect.

Exclamation mark expansion

Exclamation mark expansion replaces an exclamation mark (!) in the PS1 prompt with the history number of the next command. However, yash-rs does not yet support command history, so this feature is currently non-functional.

To include a literal exclamation mark in the prompt, use a double exclamation mark (!!).

Compatibility

POSIX.1-2024 allows shells to perform exclamation mark expansion before other expansions, in which case exclamation marks produced by those expansions are not replaced.

Additional special notation that starts with a backslash (\), supported by earlier versions of yash, is not yet implemented in yash-rs.

Job control

Job control lets users selectively stop (suspend) commands and resume them later. It also allows users to interrupt running commands.

Job control is intended for use in interactive shells. Unless otherwise noted, the descriptions below assume the shell is interactive.

Overview

Let’s first get a general idea of job control.

Why is job control useful?

Suppose you start a command to download a file, but realize the network is down and want to cancel the download. With job control, you can interrupt the command by pressing Ctrl-C, which usually terminates it:

$ curl 'http://example.com/largefile.zip' > largefile.zip
^C
$

Job control also lets you move running commands between the foreground and background, making it easier to manage multiple tasks. For example, suppose you run a command to remove many files:

$ rm -rf ~/.my_cache_files

If the command is taking too long, you might wish you had started it as an asynchronous command so you could run other commands at the same time. With job control, you can turn this running command into an asynchronous one without stopping and restarting it. First, press Ctrl-Z to suspend the command and return to the shell prompt. The shell displays a message indicating the command has been stopped:

^Z[1] + Stopped(SIGTSTP)     rm -rf ~/.my_cache_files

You can then resume the command in the background by typing:

$ bg
[1] rm -rf ~/.my_cache_files

Now the command runs asynchronously, and you can continue using the shell for other tasks. If you want to bring the command back to the foreground (synchronous execution), use the fg built-in:

$ fg
rm -rf ~/.my_cache_files

Another common scenario is when you use an editor to work on source code and want to build and test your code while keeping the editor open. You can suspend the editor with Ctrl-Z, run your build command, and then return to the editor with fg:

$ vi main.rs
^Z[1] + Stopped(SIGTSTP)     vi main.rs
$ cargo build
$ fg
vi main.rs

(This example only shows the shell output. In practice, you would also see the editor screen and the build command’s output.)

How job control works

The shell implements job control using the operating system’s process management features. It manages processes and their states, allowing users to control their execution.

When the shell starts a subshell, it runs the subshell in a separate process. This process is placed in a new process group, so any processes created during the subshell’s execution can be managed together. Process groups allow the shell and other utilities to send signals to all relevant processes at once.

If the subshell runs synchronously (in the foreground), the shell sets its process group as the terminal’s foreground process group. This lets you interact with the subshell’s processes while they’re running. When you press Ctrl-C or Ctrl-Z, the terminal sends a SIGINT or SIGTSTP signal to the foreground process group. Typically, SIGINT terminates a process, and SIGTSTP suspends it.

When a foreground process is suspended, the shell displays a message and returns to the prompt. The shell keeps a list of remaining subshells (jobs) so you can manage them later. When you use the fg built-in, the shell makes the specified job the foreground process group again and sends it a SIGCONT signal to resume execution. If you use bg, the shell sends SIGCONT but leaves the job running in the background.

All commands in a pipeline run in the same process group, so you can manage the entire pipeline as a single job.

When the shell is reading input, it makes itself the terminal’s foreground process group. This means key sequences like Ctrl-C and Ctrl-Z send signals to the shell itself. However, the shell ignores these signals to avoid being interrupted or suspended unintentionally.

Enabling job control

By default, job control is enabled only if the shell is interactive. You can enable or disable job control at startup or during a shell session by specifying the monitor shell option:

yash3 -o monitor

Creating and managing jobs

Job control is complex. yash-rs implements it mostly according to the POSIX.1-2024 standard, with some deviations. Non-POSIX behavior is marked with ⚠️.

Job control concepts

A process is a running instance of a program, such as the shell or an external utility. Each process belongs to a process group, which is a collection of processes managed together. Each process group belongs to a session, which is a collection of process groups.

When a process creates another process, the new process is its child process, and the original is the parent process. Child processes inherit certain attributes, such as process group and session, but can also create new ones.

In the context of job control, a terminal is an abstract interface managed by the operating system, which provides the necessary mechanisms for shells to implement job control. A terminal can be associated with a session, making it a controlling terminal. A process group can be selected as the terminal’s foreground process group, which receives signals from key sequences like Ctrl-C and Ctrl-Z. Other process groups in the session are background process groups.

For this document, we assume all terminals are controlling terminals, since non-controlling terminals aren’t useful for job control.

A job is a subshell implemented as a child process of the shell. (⚠️This differs from POSIX, which uses “job” for a broader set of commands, including lists.) Each job has a unique job number, a positive integer assigned by the shell when the job is created. The shell maintains a job list with information about each job’s number, status, etc.

A job ID starts with % and is used to specify jobs in built-ins like fg, bg, and jobs. For example, %1 refers to job number 1.

Subshells and process groups

When job control is enabled, the shell manages each subshell as a job in a new process group, allowing independent control. A multi-command pipeline is treated as a single job, with all commands in the same process group. ⚠️Subshells created for command substitutions are not treated as jobs and do not create new process groups, because yash-rs does not support suspending and resuming entire commands containing command substitutions.

Job control does not affect nested subshells recursively. However, if a subshell starts another shell that supports job control, that shell can manage jobs independently.

You can view job process groups using the ps utility:

$ sleep 60 && echo "1 minute elapsed!"&
[1] 10068
$ ps -j
    PID    PGID     SID TTY          TIME CMD
  10012   10012   10012 pts/1    00:00:00 yash3
  10068   10068   10012 pts/1    00:00:00 yash3
  10069   10068   10012 pts/1    00:00:00 sleep
  10076   10076   10012 pts/1    00:00:00 ps

Foreground and background jobs

Unless starting an asynchronous command, the shell runs jobs as the terminal’s foreground process group. This directs signals from key sequences like Ctrl-C and Ctrl-Z to the job, not the shell or background jobs.

For example, pressing Ctrl-C interrupts a foreground job (the signal is invisible, but ^C shows you pressed Ctrl-C):

$ sleep 60
^C$ 

When a foreground job terminates or suspends, the shell returns itself to the foreground so it can continue running commands and reading input. The shell can only examine the status of its direct child processes; descendant processes do not affect job control.

Here’s how to suspend a foreground job with Ctrl-Z (^Z shows you pressed Ctrl-Z):

$ sleep 60
^Z[1] + Stopped(SIGTSTP)     sleep 60
$ 

An asynchronous command creates a background job, which runs alongside the shell and other jobs. The shell shows the job number and process (group) ID when the background job is created:

$ sleep 60&
[1] 10068
$ 

Background jobs are not affected by Ctrl-C or Ctrl-Z. To send signals to background jobs, use the kill built-in (see Signaling jobs). You can also bring a background job to the foreground with fg and then use Ctrl-C or Ctrl-Z.

Suspending foreground jobs

Pressing Ctrl-Z sends a SIGTSTP signal to the foreground process group. Processes may respond differently, but typically suspend execution.

When a foreground job suspends, the shell displays a message and discards any pending commands that have been read but not yet executed. This prevents the shell from running commands that might depend on the suspended job’s result. (⚠️POSIX.1-2024 allows discarding only up to the next asynchronous command, but yash-rs discards all pending commands.)

For example, sleep is suspended and the following echo is discarded:

$ sleep 60 && echo "1 minute elapsed!"
^Z[1] + Stopped(SIGTSTP)     sleep 60
$ 

To avoid discarding remaining commands, run the sequence in a subshell. Here, the subshell is suspended during sleep, and echo runs after sleep resumes and finishes:

$ (sleep 60 && echo "1 minute elapsed!")
^Z[1] + Stopped(SIGTSTP)     sleep 60 && echo "1 minute elapsed!"
$ fg
sleep 60 && echo "1 minute elapsed!"
1 minute elapsed!

After suspension, the ? special parameter shows the exit status of the suspended job as if it had been terminated by the signal that suspended it:

$ sleep 60
^Z[1] + Stopped(SIGTSTP)     sleep 60
$ echo "Exit status $? corresponds to SIG$(kill -l $?)"
Exit status 404 corresponds to SIGTSTP

Resuming jobs

The fg built-in brings a job to the foreground and sends it a SIGCONT signal to resume execution. The job continues as the terminal’s foreground process group, letting you interact with it again:

$ sleep 60 && echo "1 minute elapsed!"&
[1] 10051
$ fg
sleep 60 && echo "1 minute elapsed!"
^Z[1] + Stopped(SIGTSTP)     sleep 60 && echo "1 minute elapsed!"
$ fg
sleep 60 && echo "1 minute elapsed!"
1 minute elapsed!

The bg built-in sends SIGCONT to a job without bringing it to the foreground, letting it continue in the background while you use the shell for other tasks.

For example, echo prints a message while the shell is in the foreground:

$ (sleep 60 && echo "1 minute elapsed!")
^Z[1] + Stopped(SIGTSTP)     sleep 60 && echo "1 minute elapsed!"
$ bg
[1] sleep 60 && echo "1 minute elapsed!"
$ echo "Background job running"
Background job running
$ 1 minute elapsed!

Signaling jobs

The kill built-in sends a signal to a process or process group. It accepts job IDs (see below) to specify jobs as targets. This allows you to control jobs at a low level, such as suspending or terminating them. For example, use kill -STOP %1 to suspend job 1, or kill -KILL %2 to terminate job 2:

$ sleep 60 && echo "1 minute elapsed!"&
[1] 10053
$ kill %1
[1] + Killed(SIGTERM)      sleep 60 && echo "1 minute elapsed!"
$ 

Job list

The job list includes each job’s number, process (group) ID, status, and command string. The shell updates this list as jobs are created, suspended, resumed, or terminated. The process group ID of a job equals the process ID of its main process, so they are not distinguished in the job list.

Use the jobs built-in to display the current job list:

$ rm -r foo& rm -r bar& rm -r baz&
[1] 10055
[2] 10056
[3] 10057
$ jobs
[1] + Running              rm -r foo
[2] - Running              rm -r bar
[3]   Running              rm -r baz

When a foreground job terminates, the shell removes it from the job list. If a job terminates in the background, the shell keeps it in the list so you can see its status and retrieve its exit status later. Such jobs are removed when their result is retrieved using jobs or wait.

Job numbers

When a job is created, the shell assigns it a unique job number, regardless of whether job control is enabled. Job numbers are assigned sequentially, starting from 1. After a job is removed, its number may be reused.

Current and previous jobs

The shell automatically selects two jobs as the current job and previous job from the job list. These can be referred to with special job IDs (see below). Some built-ins operate on the current job by default, making it easy to specify jobs without typing a job number or command string.

In job IDs and jobs output, the current job is marked with +, and the previous job with -.

The current job is usually the most recently suspended job, or another job if none are suspended. When a job is suspended, it becomes the current job, and the previous current job becomes the previous job. When a suspended job is resumed or removed, the current and previous jobs are updated so the current job is always a suspended job if any exist, and the previous job is another suspended job if possible. If there is only one job, there is no previous job. These rules ensure built-ins like fg and bg operate on the most relevant jobs by default.

Job IDs

Built-in utilities that operate on jobs use job IDs to specify them. A job ID matches one of these formats:

  • %, %%, or %+: the current job.
  • %-: the previous job.
  • %n: job number n.
  • %foo: job with a command string starting with foo.
  • %?foo: job with a command string containing foo.

Job status change notifications

When a background job’s status changes (suspended, resumed, or terminated), the shell automatically notifies you before the next command prompt, so you can see job status changes without checking manually. The notification format matches the jobs built-in output.

$ rm -r foo& # remove a directory in the background
[1] 10059
$ rm -r bar # remove another directory in the foreground
[1] - Done                 rm -r foo
$ 

In this example, the rm -r foo job finishes while rm -r bar runs in the foreground. The background job’s status change is automatically shown before the next prompt.

Additional details

The following sections cover special cases and extra features of job control you may not need in everyday use.

Terminal setting management

⚠️Not yet implemented in yash-rs: Some utilities, like less and vi, change terminal settings for interactive use and complex UI. If suspended, they may leave the terminal in an unsuitable state. To prevent this, the shell should restore the terminal settings when a foreground job is suspended, and again when the job is resumed in the foreground.

Job control in non-interactive shells

You can enable job control in non-interactive shells, but it’s rarely useful. Job control is mainly for interactive use, where users manage jobs dynamically. In non-interactive shells, there’s no user interaction, so features like suspending and resuming jobs don’t apply.

When job control is enabled in a non-interactive shell:

  • The shell does not ignore SIGINT, SIGTSTP, or other job control signals by default. The shell itself may be interrupted or suspended with Ctrl-C or Ctrl-Z.
  • The shell does not automatically notify you of job status changes. You must use the jobs built-in to check status.

Jobs without job control

Each asynchronous command started when job control is disabled is also managed as a job, but runs in the same process group as the shell. Signals from key sequences like Ctrl-C and Ctrl-Z are sent to the whole process group, including the shell and the asynchronous command. This means jobs cannot be interrupted, suspended, or resumed independently. The shell still assigns job numbers and maintains the job list so you can see status and retrieve exit status later.

Background shells

When a shell starts job control in the background, it suspends itself until brought to the foreground by another process. This prevents the shell from interfering with the current foreground process group. (⚠️POSIX.1-2024 requires using SIGTTIN for this, but yash-rs uses SIGTTOU instead. See Issue #421 for details.)

⚠️POSIX.1-2024 requires the shell to become a process group leader — the initial process in a process group — when starting job control. Yash-rs does not currently implement this. See Issue #483 for why this is not straightforward.

Configuring key sequences for signals

You can configure key sequences that send signals to the foreground process group using the stty utility. The table below shows parameter names, default key sequences, and corresponding signals:

ParameterKeySignal
intrCtrl-CSIGINT (interrupt)
suspCtrl-ZSIGTSTP (suspend)
quitCtrl-\SIGQUIT (quit)

For example, to change the intr key to Ctrl-X:

$ stty intr ^X

If your terminal uses different key sequences, press the appropriate keys instead of Ctrl-C or Ctrl-Z to send signals to the foreground process group.

View the current configuration with stty -a.

Compatibility

POSIX.1-2024 defines job control but allows for implementation-defined behavior in many areas. Yash-rs follows the standard closely, with some deviations (marked with ⚠️). Job control is complex, and implementations differ. Perfect POSIX compliance is not expected in any shell, including yash-rs.

The job ID % is a common extension to POSIX.1-2024. The strictly portable way to refer to the current job is %% or %+.

Built-in utilities

Built-in utilities (or built-ins) are utilities built into the shell, not separate executables. They run directly in the shell process, making them faster and more efficient for certain tasks.

Types of built-in utilities

Yash provides several types of built-in utilities.

Special built-ins

Special built-ins have special meaning or behavior in the shell. They are used for control flow, variable manipulation, and other core tasks. Notable properties:

  • Command search always finds special built-ins first, regardless of PATH.
  • Functions cannot override special built-ins.
  • Assignments in a simple command running a special built-in persist after the command.
  • Errors in special built-ins cause the shell to exit if non-interactive.

POSIX.1-2024 defines these special built-ins:

  • . (dot)
  • : (colon)
  • break
  • continue
  • eval
  • exec
  • exit
  • export
  • readonly
  • return
  • set
  • shift
  • times
  • trap
  • unset

As an extension, yash-rs also supports source as an alias for . (dot).

Mandatory built-ins

Mandatory built-ins must be implemented by all POSIX-compliant shells. They provide essential scripting and command features.

Like special built-ins, they are found regardless of PATH in command search, but they can be overridden by functions.

POSIX.1-2024 defines these mandatory built-ins:

  • alias
  • bg
  • cd
  • command
  • fc (not yet implemented)
  • fg
  • getopts
  • hash (not yet implemented)
  • jobs
  • kill
  • read
  • type
  • ulimit
  • umask
  • unalias
  • wait

Elective built-ins

Elective built-ins work like mandatory built-ins but are not required by POSIX.1-2024. They provide extra features for scripting or interactive use.

Elective built-ins can be overridden by functions and are found in command search regardless of PATH.

In yash-rs, the following elective built-in is implemented:

  • typeset

More may be added in the future.

Substitutive built-ins

Substitutive built-ins replace external utilities to avoid process creation overhead for common tasks.

Substitutive built-ins behave like external utilities: they are located during command search and can be overridden by functions. However, the built-in is only available if the corresponding external utility exists in PATH. If the external utility is missing from PATH, the built-in is also unavailable, ensuring consistent behavior with the absence of the utility.

Yash-rs implements these substitutive built-ins:

  • false
  • pwd
  • true

More may be added in the future.

Compatibility

POSIX.1-2024 reserves many names for shell built-ins. Yash-rs implements some of them; others may not be implemented or may differ in other shells.

  • alloc
  • autoload
  • bind
  • bindkey
  • builtin
  • bye
  • caller
  • cap
  • chdir
  • clone
  • comparguments
  • compcall
  • compctl
  • compdescribe
  • compfiles
  • compgen
  • compgroups
  • complete
  • compound
  • compquote
  • comptags
  • comptry
  • compvalues
  • declare
  • dirs
  • disable
  • disown
  • dosh
  • echotc
  • echoti
  • enum
  • float
  • help
  • hist
  • history
  • integer
  • let
  • local
  • login
  • logout
  • map
  • mapfile
  • nameref
  • popd
  • print
  • pushd
  • readarray
  • repeat
  • savehistory
  • shopt
  • source
  • stop
  • suspend
  • typeset
  • whence

Command line argument syntax conventions

Arguments are string parameters passed to built-in utilities. The syntax varies between built-ins, but most follow common conventions.

Operands

Operands are main arguments specifying objects or values for the built-in. For example, in cd, the operand is the directory:

$ cd /dev

Options

Options are supplementary arguments that modify the behavior of the built-in. They start with a hyphen (-) followed by one or more characters. Short options are named with a single character (e.g., -P), while long options are more descriptive and start with two hyphens (e.g., --physical). For example, the cd built-in uses -P or --physical to force the shell to use the physical directory structure instead of maintaining symbolic links:

$ cd -P /dev

With a long option:

$ cd --physical /dev

Multiple short options can be combined. For example, cd -P -e /dev can be written as:

$ cd -Pe /dev

Long options must be specified separately.

Long option names can be abbreviated if unambiguous. For example, --p is enough for --physical in cd:

$ cd --p /dev

However, future additions may make abbreviations ambiguous, so use the full name in scripts.

POSIX.1-2024 only specifies short option syntax. Long options are a yash-rs extension.

Option arguments

Some options require an argument. For short options, the argument can follow immediately or as a separate argument. For example, -d in read takes a delimiter argument:

$ mkdir $$ && cd $$ || exit
$ echo 12 42 + foo bar > line.txt
$ read -d + a b < line.txt
$ echo "A: $a, B: $b"
A: 12, B: 42

If the argument is non-empty, it can be attached: -d+. If empty, specify separately: -d '':

$ mkdir $$ && cd $$ || exit
$ echo 12 42 + foo bar > line.txt
$ read -d+ a b < line.txt
$ echo "A: $a, B: $b"
A: 12, B: 42
$ read -d '' a b < line.txt
$ echo "A: $a, B: $b"
A: 12, B: 42 + foo bar

For long options, use = or a separate argument:

$ mkdir $$ && cd $$ || exit
$ echo 12 42 + foo bar > line.txt
$ read --delimiter=+ a b < line.txt
$ echo "A: $a, B: $b"
A: 12, B: 42
$ read --delimiter + a b < line.txt
$ echo "A: $a, B: $b"
A: 12, B: 42

Separators

To treat an argument starting with - as an operand, use the -- separator. This tells the shell to stop parsing options. For example, to change to a directory named -P:

$ mkdir $$ $$/-P && cd $$ || exit
$ cd -- -P

Note that a single hyphen (-) is not an option, but an operand. It can be used without --:

$ cd /tmp
$ cd /
$ cd -
/tmp

Argument order

Operands must come after options. All arguments after the first operand are treated as operands, even if they start with a hyphen:

$ cd /dev -P
error: unexpected operand
 --> <stdin>:1:9
  |
1 | cd /dev -P
  | --      ^^ -P: unexpected operand
  | |
  | info: executing the cd built-in
  |

Specifying options after operands may be supported in the future.

Alias built-in

The alias built-in defines aliases or prints alias definitions.

Synopsis

alias [name[=value]…]

Description

The alias built-in defines aliases or prints existing alias definitions, depending on the operands. With no operands, it prints all alias definitions in a quoted assignment form suitable for reuse as input to alias.

Options

None.

Non-POSIX options may be added in the future.

Operands

Each operand must be of the form name=value or name. The first form defines an alias named name that expands to value. The second form prints the definition of the alias named name.

Errors

It is an error if an operand without = refers to an alias that does not exist.

Exit status

Zero unless an error occurs.

Examples

See Aliases.

Compatibility

The alias built-in is specified by POSIX.1-2024.

Some shells have predefined aliases that are printed even if you have not defined any explicitly.

Pattern matching

This section describes the pattern matching notation used in the shell. Patterns are used in pathname expansion, case commands, and parameter expansion modifiers.

Literals

A literal is a character that matches itself. For example, the pattern a matches the character a. All characters are literals except for the special characters described below.

Quoting

Quoting makes a special character behave as a literal. See the Quoting section for details. Additionally, for unquoted parts of a pattern produced by parameter expansion, command substitution, or arithmetic expansion, backslashes escape the following character, but such backslashes are not subject to quote removal.

In this example, no pathname expansion occurs because the special characters are quoted:

$ echo a\*b
a*b
$ asterisk='*'
$ echo "$asterisk"
*
$ quoted='a\*b'
$ echo $quoted
a\*b

Special characters

The following characters have special meanings in patterns:

  • ? – Matches any single character.
  • * – Matches any number of characters, including none.
  • […] – Matches any single character from the set of characters inside the brackets. For example, [abc] matches a, b, or c. Ranges can be specified with a hyphen, like [a-z] for lowercase letters.
  • [!…] and [^…] – Matches any single character not in the set of characters inside the brackets. For example, [!abc] matches any character except a, b, or c.

The [^…] form is not supported in all shells; prefer using [!…] for compatibility.

$ echo ?????? # prints all six-character long filenames
Videos
$ echo Do* # prints all files starting with Do
Documents Downloads
$ echo [MP]* # prints all files starting with M or P
Music Pictures
$ echo *[0-9] # prints all files ending with a digit
foo.bak.1 foo.bak.2 bar.bak.3

Special elements in brackets

Bracket expressions […] can include special elements:

  • Character classes: [:class:] matches any character in the specified class. Available classes:

    • [:alnum:] – Alphanumeric characters (letters and digits)
    • [:alpha:] – Alphabetic characters (letters)
    • [:blank:] – Space and tab characters
    • [:cntrl:] – Control characters
    • [:digit:] – Digits (0-9)
    • [:graph:] – Printable characters except space
    • [:lower:] – Lowercase letters
    • [:print:] – Printable characters including space
    • [:punct:] – Punctuation characters
    • [:space:] – Space characters (space, tab, newline, etc.)
    • [:upper:] – Uppercase letters
    • [:xdigit:] – Hexadecimal digits (0-9, a-f, A-F)
    $ echo [[:upper:]]* # prints all files starting with an uppercase letter
    Documents Downloads Music Pictures Videos
    $ echo *[[:digit:]~] # prints all files ending with a digit or tilde
    foo.bak.1 foo.bak.2 bar.bak.3 baz~
    
  • Collating elements: [.char.] matches the collating element char. A collating element is a character or sequence of characters treated as a single unit in pattern matching. Collating elements depend on the current locale and are not yet implemented in yash-rs.

  • Equivalence classes: [=char=] matches the equivalence class of char. An equivalence class is a set of characters considered equivalent for matching purposes (e.g., a and A in some locales). This feature is not yet implemented in yash-rs.

Locale support is not yet implemented in yash-rs. Currently, all patterns match the same characters regardless of locale. Collating elements and equivalence classes simply match the characters as they are, without any special treatment.

Special considerations for pathname expansion

See the Pathname expansion section for additional rules that apply in pathname expansion.

Arithmetic expressions

Arithmetic expressions in arithmetic expansion are similar to C expressions. They can include numbers, variables, operators, and parentheses.

Numeric constants

Numeric constants can be:

  • Decimal, written as-is (e.g., 42)
  • Octal, starting with 0 (e.g., 042)
  • Hexadecimal, starting with 0x or 0X (e.g., 0x2A)
$ echo $((42))   # decimal
42
$ echo $((042))  # octal
34
$ echo $((0x2A)) # hexadecimal
42

All integers are signed 64-bit values, ranging from -9223372036854775808 to 9223372036854775807.

C-style integer suffixes (U, L, LL, etc.) are not supported.

Floating-point constants are not supported, but may be added in the future.

Variables

Variable names consist of Unicode alphanumerics and ASCII underscores, but cannot start with an ASCII digit. Variables must have numeric values.

$ a=5 b=10
$ echo $((a + b))
15

If a variable is unset and the nounset shell option is off, it is treated as zero:

$ unset x; set +o nounset
$ echo $((x + 3))
3

If the nounset option is on, an error occurs when trying to use an unset variable:

$ unset x; set -o nounset
$ echo $((x + 3))
error: cannot expand unset parameter
 --> <arithmetic_expansion>:1:1
  |
1 | x + 3
  | ^ parameter `x` is not set
  |
 ::: <stdin>:2:6
  |
2 | echo $((x + 3))
  |      ---------- info: arithmetic expansion appeared here
  |
  = info: unset parameters are disallowed by the nounset option

If a variable has a non-numeric value, an error occurs.

$ x=foo
$ echo $((x + 3))
error: error evaluating the arithmetic expansion
 --> <arithmetic_expansion>:1:1
  |
1 | x + 3
  | ^ invalid variable value: "foo"
  |
 ::: <stdin>:2:6
  |
2 | echo $((x + 3))
  |      ---------- info: arithmetic expansion appeared here
  |

Currently, variables in arithmetic expressions must have a single numeric value. In the future, more complex values may be supported.

Operators

The following operators are supported, in order of precedence:

  1. ( ) – grouping
  2. Postfix:
    • ++ – increment
    • -- – decrement
  3. Prefix:
    • + – no-op
    • - – numeric negation
    • ~ – bitwise negation
    • ! – logical negation
    • ++ – increment
    • -- – decrement
  4. Binary (left associative):
    • * – multiplication
    • / – division
    • % – modulus
  5. Binary (left associative):
    • + – addition
    • - – subtraction
  6. Binary (left associative):
    • << – left shift
    • >> – right shift
  7. Binary (left associative):
    • < – less than
    • <= – less than or equal to
    • > – greater than
    • >= – greater than or equal to
  8. Binary:
    • == – equal to
    • != – not equal to
  9. Binary:
    • & – bitwise and
  10. Binary:
    • | – bitwise or
  11. Binary:
    • ^ – bitwise xor
  12. Binary:
    • && – logical and
  13. Binary:
    • || – logical or
  14. Ternary (right associative):
    • ? : – conditional expression
  15. Binary (right associative):
    • = – assignment
    • += – addition assignment
    • -= – subtraction assignment
    • *= – multiplication assignment
    • /= – division assignment
    • %= – modulus assignment
    • <<= – left shift assignment
    • >>= – right shift assignment
    • &= – bitwise and assignment
    • |= – bitwise or assignment
    • ^= – bitwise xor assignment

Other operators, such as sizeof, are not supported.