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 . built-in 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.

Versioning and compatibility

POSIX conformance

POSIX (Portable Operating System Interface) is a set of standards specified by the IEEE to ensure compatibility among Unix-like operating systems. It defines a standard operating system interface and environment, including command-line utilities, shell scripting, and system calls.

As of 2025, the latest version of POSIX is POSIX.1-2024. The requirements for shells are mainly documented in the Shell & Utilities volume. Yash-rs aims to comply with the POSIX standard, providing a consistent and portable environment for shell scripting and command execution.

The shell currently supports running shell scripts and basic interactive features in a POSIX-compliant manner. See the homepage for an overview of implemented features. Progress on POSIX conformance and feature implementation is tracked in GitHub Issues and the GitHub Project.

Many features of yash-rs are still under development, and some may not yet be fully compliant with the POSIX standard. Any non-conforming behavior is described in the Compatibility section of each feature’s documentation. These sections also clarify which aspects of the shell’s behavior are POSIX requirements and which are shell-specific extensions.

Yash and yash-rs

Yash-rs is the successor to yash, which is also a POSIX-compliant shell supporting both scripting and interactive use. Since yash-rs currently lacks advanced interactive features such as command history and line editing, yash is recommended for interactive use at this time.

Current releases of yash use version numbers in the form 2.x (or sometimes 2.x.y), where x increases with each release. Earlier versions used 1.x or 0.x. To avoid version number conflicts, the 3.x series is reserved for yash-rs, and there are no plans for yash to use 3.x version numbers.

In this manual, “yash” may refer to both yash and yash-rs collectively. When a distinction is needed, we refer to the former as “previous versions of yash”, “older versions of yash”, and so on.

Yash-rs versioning policy

Yash-rs follows Semantic Versioning 2.0.0. Each release has a version number consisting of three parts separated by dots, such as 3.0.1. The first part is the major version, incremented for breaking changes. The second is the minor version, incremented for new features that do not break compatibility. The third is the patch version, incremented for bug fixes that do not introduce new features.

Semantic versioning applies to the observable behavior of yash-rs as documented in this manual. This includes (but is not limited to) the syntax and semantics of the shell language, shell startup and termination behavior, and the behavior of built-in utilities. The major version will be incremented if any of these behaviors change in a way that is not backward compatible.

We strive to minimize breaking changes by carefully defining the scope of behavior covered by semantic versioning. In this manual, we may declare certain behaviors as “subject to change”, “may change in the future”, and similar phrases to reserve the right to make changes without incrementing the major version. Additionally, the following changes are not considered breaking:

  • Changes to undocumented or internal behavior
  • New features that were previously documented as “not yet implemented/supported”
  • New features that allow commands which previously resulted in errors to succeed
  • Changes that make non-conforming behavior compliant with POSIX

See the changelog for a detailed list of changes and updates to yash-rs. Note that the documentation at https://magicant.github.io/yash-rs/ is deployed from the master branch and may include unreleased changes.

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.

Aliases

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

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:

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.

Dollar signs

A dollar sign ($) may introduce a dollar single quote, parameter expansion, command substitution, or arithmetic expansion, depending on the following characters. If the dollar sign is not followed by a valid sequence for any of these expansions, it is treated as a literal character.

Dollar signs that are immediately followed by a non-whitespace character and do not form a valid expansion sequence are currently treated as literal characters, but this usage may change in future versions of the shell. For example, $% and $+ may be interpreted as special parameters in the future.

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:

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 Command substitution and Arithmetic expansion.

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:

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
  • Any words that end with a colon (:)

These 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 is delimited by a slash (/). 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:

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.

Compatibility

When a command substitution only contains redirections, some shells treat it specially. For example, $(<file) is equivalent to $(cat file) in bash and zsh, but yash-rs does not support this yet.

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

Note the difference between such parameters, which are expanded before the arithmetic expansion is evaluated, and variables that are part of the expression:

$ 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 non-literal pattern 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.

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 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 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 in command search

  • 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).
    • Many shells set the default value to # (a hash sign followed by a space) for the root user. This is not yet implemented in yash-rs.
  • 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

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 in the background.
    • The value is 0 until any asynchronous command is executed in the current shell environment. However, the behavior may be changed in the future so that it works like an unset parameter.
  • 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.
  • Assignment words must come before a command name if present.
  • 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 working 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 true 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.

POSIX allows caching pathnames found during command search, but yash-rs does not implement this yet.

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 before running a target, the exit status is non-zero. Specifically:
    • 127 if command search failed
    • 126 if the target was identified but 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.

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:

$ mkdir $$ && cd $$ || exit
$ printf 'This is the first line of file1.\n' > file1.txt
$ printf 'This is the first line of file2.\n' > file2.txt
$ printf '\n\n\n\n\n\n\n\n\n' >> file1.txt
$ printf '\n\n\n\n' >> file2.txt
$ 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.

Each asynchronous command runs in a subshell. Changes to the shell environment (like variable assignments) made in an asynchronous command do not affect the parent shell. 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, where background reads from the terminal typically cause the job to be stopped by SIGTTIN.

$ 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 non-zero if the function name expansion fails or if a read-only function with the same name exists.

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

Read-only functions

Make a function read-only with the typeset built-in. Read-only 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 read-only in yash-rs.

Showing function definitions

To display the definition of a function, use the typeset built-in with the -f and -p options:

$ greet() {
>     echo "Hello, World!"
> }
$ typeset -fp greet
greet() { echo "Hello, World!"; }

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.

$ mkdir $$ && cd $$ || exit
$ echo "Hello, world!" > greet.txt
$ alias list=ls
$ list
greet.txt

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

(The read built-in reads a line from standard input into a variable.)

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 regular file or opens the existing non-regular 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:

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

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

$ user="Alice"
$ cat <<EOF
> Hello, '$user'!
> EOF
Hello, 'Alice'!

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.

  • 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. See Job status change notifications for details.

    • ⚠️ 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

Working directory

The working directory is the directory from which relative paths are resolved. It is a property of the shell environment and is inherited by subshells and external utilities.

Many commands use the working directory as the default location for file operations. For example, specifying a relative path in a redirection creates the file in the working directory. The ls utility lists files in the working directory if no operand is given.

$ mkdir $$ && cd $$ || exit
$ echo "Hello, world!" > hello.txt
$ ls
hello.txt

Viewing the current working directory

The PWD and OLDPWD variables hold the absolute pathnames of the current and previous working directories, respectively. These variables are updated automatically when the working directory changes. If you modify or unset them manually, automatic updates are no longer guaranteed.

$ echo "$PWD"
/home/user

You can also use the pwd built-in to print the current working directory.

$ pwd
/home/user

Changing the working directory

Use the cd built-in to change the working directory.

$ cd /tmp
$ pwd
/tmp
$ cd /dev
$ echo "$PWD" "$OLDPWD"
/dev /tmp

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. See Command line argument syntax conventions for how arguments are parsed into options and operands.

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, and
  • 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 the command name and shell options.

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 shell option is enabled. (See Exiting on errors.)

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 (in yash-rs).

$ 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. (However, POSIX allows the shell to execute the new trap if the EXIT trap is redefined while it is running.)

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. Such a shell is called an interactive shell. 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.

Note that subshells of an interactive shell are not interactive, even if the interactive option is set.

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 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 with the bg built-in:

$ 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 command 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 -s STOP %1 to suspend job 1, or kill -s 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.

Note that automatic notifications do not remove the reported job from the job list; jobs are only removed after their status is retrieved using the jobs or wait built-ins.

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 a state unsuitable for other utilities to run. 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 %+.

In yash-rs, jobs are currently defined as subshells, whereas POSIX.1-2024 treats a wider range of commands as jobs. At present, yash-rs does not support suspending built-ins, since they run within the current shell environment rather than in a subshell. Future versions of yash-rs may expand the definition of jobs to include this capability.

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. (See Shell errors)

POSIX.1-2024 defines these special built-ins:

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:

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:

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:

More may be added in the future.

Compatibility

POSIX.1-2024 reserves many names for shell-specific built-ins. Yash-rs implements some of these, and may add more in the future. Other shells may implement these differently:

  • 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. The description below applies to yash-rs built-ins unless otherwise noted.

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 preserving 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.

Colon (:) built-in

The colon (:) built-in does nothing.

Synopsis

: […]

Description

The colon built-in is a dummy command that does nothing. Any arguments are ignored.

Errors

None.

Exit status

Zero.

Compatibility

The colon built-in is specified in the POSIX standard.

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.

Bg built-in

The bg built-in resumes suspended jobs in the background.

Synopsis

bg [job_id…]

Description

See Job control for an overview of job control in yash-rs. The built-in resumes the specified jobs by sending the SIGCONT signal to them.

The (last) resumed job’s process ID is set to the ! special parameter.

Options

None.

Operands

Operands specify jobs to resume as job IDs. If omitted, the built-in resumes the current job.

Standard output

The built-in writes the job number and name of each resumed job to the standard output.

Errors

It is an error if:

  • the specified job is not found,
  • the specified job is not job-controlled, that is, job control was off when the job was started, or
  • job control is off in the current shell environment.

Exit status

Zero unless an error occurs.

Examples

See Job control.

Compatibility

Many implementations allow omitting the leading % from job IDs, though it is not required by POSIX. Previous versions of yash allowed it, but this is not yet supported in yash-rs.

Some implementations (including the previous version of yash, but not this version) regard it is an error to resume a job that has already terminated.

Break built-in

The break built-in terminates the execution of a loop.

Synopsis

break [n]

Description

break n quits the execution of the nth innermost for, while, or until loop. The specified loop must lexically enclose the break command, that is:

It is an error if there is no loop enclosing the break command. If n is greater than the number of enclosing loops, the built-in exits the outermost one.

Options

None.

Operands

Operand n specifies the nest level of the loop to exit. If omitted, it defaults to 1. It is an error if the value is not a positive decimal integer.

Exit status

Zero if the built-in successfully breaks the loop; non-zero otherwise.

Examples

See Break and continue.

Compatibility

The break built-in is specified by POSIX.1-2024.

The behavior is unspecified in POSIX when the break built-in is used without an enclosing loop, in which case the current implementation returns an error.

POSIX allows the built-in to break a loop running in the current execution environment that does not lexically enclose the break command. Our implementation does not do that.

In some shells, the break built-in lacks support for the -- separator.

Previous versions of yash supported the non-standard -i option, but this is not yet supported in yash-rs.

Cd built-in

The cd built-in changes the working directory.

Synopsis

cd [-L|-P [-e]] [directory]

Specifying directory

The built-in by default changes the working directory to the user’s home directory, which is contained in the HOME variable:

$ cd

An operand, if given, specifies the directory to change to:

$ cd /usr/bin

To return to the previous working directory, pass - as the operand:

$ cd -

Resolving relative pathnames

If the new working directory is specified as a relative pathname, it is resolved against the current working directory by default. You can make it resolved against other directories by defining the CDPATH variable. For example, if the variable is defined as CDPATH=/usr, running cd bin will change the working directory to /usr/bin. See the security implications of CDPATH before using it.

The built-in provides two modes for handling symbolic links in computing the new working directory. See Options and Examples.

Description

The built-in changes the working directory to the specified directory. The new working directory is determined from the option and operand as follows:

  1. If the operand is omitted, the value of the HOME variable is used for the operand. If the operand is a single hyphen (-), the value of the OLDPWD variable is used for the operand. If the variable is not set or empty, it is an error. Otherwise, the operand is used as is.
  2. If the operand does not start with a slash (/) and the first pathname component in the operand is neither dot (.) nor dot-dot (..), the built-in searches the directories specified by the CDPATH variable for a first directory that contains the operand as a subdirectory. If such a directory is found, the operand is replaced with the path to the subdirectory, that is, the concatenation of the pathname contained in CDPATH and the previous operand. If no such directory is found, the operand is used as is.
    • The value of CDPATH is a colon-separated list of directories, searched in order. If it includes an empty item, it is treated as the current working directory. It is similar to including . in the list, but it suppresses printing the new working directory.
    • Directories listed in CDPATH may be relative, in which case they are resolved against the current working directory. This behavior may be confusing, so use with caution.
    • Note the security implications of CDPATH.
  3. If the -L option is effective, the operand is canonicalized as follows:
    1. If the operand does not start with a slash (/), the value of the PWD variable is prepended to the operand.
    2. Dot (.) components in the operand are removed.
    3. Dot-dot (..) components in the operand are removed along with the preceding component. However, if such a preceding component refers to a non-existent directory, it is an error.
    4. Redundant slashes in the operand are removed.

The working directory is changed to the operand after the above processing. If the change is successful, the value of the PWD variable is updated to the new working directory:

  • If the -L option is effective, the final operand value becomes the new value of PWD.
  • If the -P option is effective, the new PWD value is recomputed in the same way as pwd -P does, so it does not include symbolic links.

The previous PWD value is assigned to the OLDPWD variable.

Options

With the -L (--logical) option, the operand is resolved logically, that is, the canonicalization is performed as above and symbolic link components are preserved in the new PWD value.

With the -P (--physical) option, the operand is resolved physically; the operand pathname is passed to the underlying system call without the canonicalization. The new PWD value is recomputed from the actual new working directory, so it does not include symbolic link components.

These two options are mutually exclusive. The last specified one applies if given both. The default is -L.

When -P is effective, the built-in may fail to determine the new working directory pathname to assign to PWD. By default, the exit status does not indicate the failure. If the -e (--ensure-pwd) option is given together with -P, the built-in returns exit status 1 in this case. This allows callers to detect that PWD could not be determined and handle the condition.

Operands

The built-in takes a single operand that specifies the directory to change to. If omitted, the value of HOME is used. If the operand is a single hyphen (-), the value of OLDPWD is used.

Standard output

If the new working directory is based on a non-empty item in CDPATH or the operand is a single hyphen (-), the built-in prints the new value of PWD followed by a newline to the standard output.

Errors

This built-in fails if the working directory cannot be changed, for example, in the following cases:

  • The operand does not resolve to an existing accessible directory.
  • The operand is omitted and HOME is not set or empty.
  • The operand is a single hyphen (-) and OLDPWD is not set or empty.
  • The resolved pathname of the new working directory is too long.

It is also an error if a given operand is an empty string.

If the -P option is effective, the built-in may fail to determine the new working directory pathname to assign to PWD, for example, in the following cases:

  • The new pathname is too long.
  • Some ancestor directories of the new working directory are not accessible.
  • The new working directory does not belong to the filesystem tree.

In these cases, the working directory remains changed, OLDPWD is updated to the previous value of PWD, PWD is left empty, and the exit status depends on the -e option.

The built-in may also fail if PWD or OLDPWD is read-only. In this case, the working directory remains changed, but the variable is not updated.

If the new working directory name cannot be printed to the standard output, the built-in prints a warning message to the standard error, but this does not affect the working directory change or the exit status.

Exit Status

  • If the working directory is changed successfully, the exit status is zero, except in the following cases where the exit status is one:
    • The -P and -e options are effective and the new working directory pathname cannot be determined.
    • The PWD or OLDPWD variable is read-only.
  • If the working directory cannot be changed because of an error in the underlying chdir system call, the exit status is two.
  • If the -L option is effective and canonicalization fails because of a .. component referring to a non-existent directory, the exit status is three.
  • If the operand cannot be processed because of an unset or empty HOME or OLDPWD, the exit status is four.
  • If the command arguments are invalid, the exit status is five.

Examples

Compare how -L and -P handle symbolic links:

$ ln -s /usr/bin symlink
$ cd -L symlink
$ pwd
/home/user/symlink
$ cd -L ..
$ pwd
/home/user
$ ln -s /usr/bin symlink
$ cd -L symlink
$ pwd
/home/user/symlink
$ cd -P ..
$ pwd
/usr
$ mkdir $$ && cd $$ || exit
$ ln -s /usr/bin symlink
$ cd -P symlink
$ pwd
/usr/bin
$ cd ..
$ pwd
/usr

See how CDPATH affects determining the new working directory:

$ mkdir $$ && cd $$ || exit
$ CDPATH=:/usr:/usr/local
$ mkdir bin
$ cd bin # enters the just created directory
$ cd bin # no "bin" in the new working directory, so picks /usr/bin
/usr/bin

Security considerations

Although CDPATH can be helpful if used correctly, it can catch unwary users off guard, leading to unintended changes in the behavior of shell scripts. If a shell script is executed with the CDPATH environment variable set to a directory crafted by an attacker, the script may change the working directory to an unexpected one. To ensure that the cd built-in behaves as intended, shell script writers should unset the variable at the beginning of the script. Users can configure CDPATH in their shell sessions, but should avoid exporting the variable to the environment. Users are advised to include an empty item as the first item in CDPATH to ensure that the current working directory is searched before other CDPATH directories are considered.

Because the built-in treats - as a special operand, running cd - does not necessarily change the working directory to a directory literally named -. This can produce unexpected results, especially when the operand is supplied via a parameter. For more information, see the Application usage section for the cd utility in POSIX. You can also use cd ./- to change to a directory literally named -.

By default, the built-in resolves pathnames logically (-L), while many other utilities resolve pathnames physically (as with cd -P). If you intend to use a pathname with both cd and other utilities, use the -P option to ensure consistent resolution.

Compatibility

POSIX-1.2024 defines the cd utility with the -L, -P, and -e options.

The shell sets PWD on the startup and modifies it in the cd built-in. If PWD is modified or unset otherwise, the behavior of cd and pwd is unspecified.

The error handling behavior and the exit status do not agree between existing implementations when the built-in fails because of a write error or a read-only variable error.

Other implementations may return different non-zero exit statuses in cases where this implementation would return exit statuses between 2 and 4.

POSIX allows the shell to convert the pathname passed to the underlying chdir system call to a shorter relative pathname when the -L option is in effect. This conversion is mandatory if:

  • the original operand was not longer than PATH_MAX bytes (including the terminating nul byte),
  • the final operand is longer than PATH_MAX bytes (including the terminating nul byte), and
  • the final operand starts with PWD and hence can be considered to be a subdirectory of the current working directory.

POSIX does not specify whether the shell should perform the conversion if the above conditions are not met. The current implementation does it if and only if the final operand starts with PWD.

Command built-in

The command built-in executes a utility bypassing functions. This is useful when you want to execute a utility that has the same name as a function. The built-in also has options to search for the location of the utility.

Synopsis

command [-p] name [arguments…]
command -v|-V [-p] name

Description

Without the -v or -V option, the command built-in executes the utility specified by the name with the given arguments. This is similar to the execution of a simple command, but the functions are not searched for the name.

With the -v or -V option, the built-in identifies the type of the name and, optionally, the location of the utility. The -v option prints the pathname of the utility, if found, and the -V option prints a more detailed description of the utility.

Options

The -p (--path) option causes the built-in to search for the utility in the standard search path instead of the current PATH.

The -v (--identify) option identifies the type of the command name and prints the pathname of the utility, if found.

The -V (--verbose-identify) option identifies the type of the command name and prints a more detailed description of the utility.

Operands

The name operand specifies the name of the utility to execute or identify. The arguments are passed to the utility when executing it.

Standard output

When the -v option is given, the built-in prints the following:

  • The absolute pathname of the utility, if found in the search path.
  • The utility name itself, if it is a non-substitutive built-in, function, or reserved word, hence not subject to search.
  • A command line that would redefine the alias, if the name is an alias.

When the -V option is given, the built-in describes the utility in a more descriptive, human-readable format. The exact format is not specified here as it is subject to change.

Nothing is printed if the utility is not found.

Errors

It is an error if the specified utility is not found or cannot be executed.

With the -v option, no error message is printed for the utility not found.

Exit status

Without the -v or -V option, the exit status is that of the utility executed. If the utility is not found, the exit status is 127. If the utility is found but cannot be executed, the exit status is 126.

With the -v or -V option, the exit status is 0 if the utility is found and 1 if not found.

Examples

See Replacing existing utilities.

Compatibility

POSIX requires that the name operand be specified, but many implementations allow it to be omitted, in which case the built-in does nothing.

When the utility is not found with the -v or -V option, some implementations return a non-zero exit status other than 1, especially 127.

When the utility is not found with the -V option, some implementations print an error message to the standard output while others to the standard error.

Continue built-in

The continue built-in skips the execution of a loop to the next iteration.

Synopsis

continue [n]

Description

continue n interrupts the execution of the nth innermost for, while, or until loop and resumes its next iteration. The specified loop must lexically enclose the continue command, that is:

It is an error if there is no loop enclosing the continue command. If n is greater than the number of enclosing loops, the built-in affects the outermost one.

If the affected loop is a for loop, the loop variable is updated to the next value in the list. The loop ends if there are no more values to iterate over.

If the affected loop is a while or until loop, the condition is re-evaluated.

Options

None.

Operands

Operand n specifies the nest level of the affected loop. If omitted, it defaults to 1. It is an error if the value is not a positive decimal integer.

Exit status

Zero if the built-in successfully continues the loop; non-zero otherwise.

Examples

See Break and continue.

Compatibility

The continue built-in is specified by POSIX.1-2024.

The behavior is unspecified in POSIX when the continue built-in is used without an enclosing loop, in which case the current implementation returns an error.

POSIX allows the built-in to restart a loop running in the current execution environment that does not lexically enclose the continue command. Our implementation declines to do that.

In some shells, the continue built-in lacks support for the -- separator.

Previous versions of yash supported the non-standard -i option, but this is not yet supported in yash-rs.

Eval built-in

The eval built-in evaluates the arguments as shell commands.

Synopsis

eval [command…]

Description

This built-in parses and executes the argument as a shell script in the current shell environment.

Options

None.

Operands

The operand is a command string to be evaluated. If more than one operand is given, they are concatenated with spaces between them to form a single command string.

Errors

During parsing and execution, any syntax error or runtime error may occur.

Exit status

The exit status of the eval built-in is the exit status of the last command executed in the command string. If there is no command in the string, the exit status is zero. In case of a syntax error, the exit status is 2.

Examples

See Evaluating command strings.

Security considerations

The eval built-in can be dangerous if used with untrusted input, as it can execute arbitrary commands. It is recommended to avoid using eval with user input or to sanitize the input before passing it to eval.

Compatibility

The eval built-in is specified by POSIX.1-2024.

In some shells, the eval built-in lacks support for the -- separator, treating it as an operand.

Previous versions of yash supported the non-standard -i option, but this is not yet supported in yash-rs.

Exec built-in

The exec built-in replaces the current shell process with an external utility invoked by treating the specified operands as a command. Without operands, the built-in makes redirections applied to it permanent in the current shell environment.

Synopsis

exec [name [arguments...]]

Description

When invoked with operands, the exec built-in replaces the currently executing shell process with a new process image, regarding the operands as command words to start the external utility. The first operand identifies the utility, and the other operands are passed to the utility as command-line arguments.

Without operands, the built-in does not start any utility. Instead, it makes any redirections performed in the calling simple command permanent in the current shell environment. (This is done even if there are operands, but the effect can be observed only when the utility cannot be invoked and the shell does not exit.)

Options

POSIX defines no options for the exec built-in.

The following non-portable options are yet to be implemented:

  • --as
  • --clear
  • --cloexec
  • --force
  • --help

Operands

The operands are treated as a command to start an external utility. If any operands are given, the first is the utility name, and the others are its arguments.

If the utility name contains a slash character, the shell will treat it as a path to the utility. Otherwise, the shell will search $PATH for the utility.

Errors

If the name operand is given, the named utility cannot be invoked, and the shell is not interactive, the current shell process will exit with an error.

Exit status

If the external utility is invoked successfully, it replaces the shell executing the built-in, so there is no exit status of the built-in. If the built-in fails to invoke the utility, the exit status will be 126. If there is no utility matching the first operand, the exit status will be 127.

If no operands are given, the exit status will be 0.

Examples

To make the current shell process run echo:

$ exec echo "Hello, World!"
Hello, World!

Note that the echo executed here is not the built-in, but the external utility found in the $PATH. The shell process is replaced by the echo process, so you don’t return to the shell prompt after the command.

See Persistent redirections for examples of using the exec built-in to make redirections permanent.

Compatibility

The exec built-in is part of POSIX.1-2024.

Exit built-in

The exit built-in causes the currently executing shell to exit.

Synopsis

exit [exit_status]

Description

exit exit_status makes the shell exit from the currently executing environment with the specified exit status.

The shell executes the EXIT trap, if any, before exiting, except when the built-in is invoked in the trap itself.

Options

None. (TBD: non-portable extensions)

Operands

The optional exit_status operand, if given, should be a non-negative decimal integer and will be the exit status of the exiting shell process.

Errors

If the exit_status operand is given but not a valid non-negative integer, it is a syntax error.

This implementation treats an exit_status value greater than 2147483647 as a syntax error.

Exit status

The exit_status operand specifies the exit status of the exiting shell.

If the operand is not given, the shell exits with the current exit status ($?). If the built-in is invoked in a trap, the exit status will be the value of $? before entering the trap.

In case of an error, the exit status is 2.

If the exit status indicates a signal that caused the process of the last command to terminate, the shell terminates with the same signal. See Exit status of the shell for details.

Examples

To exit the shell with exit status 42:

$ exit 42

Compatibility

The exit built-in is specified by POSIX.1-2024.

In some shells, the exit built-in lacks support for the -- separator.

The behavior is undefined in POSIX if exit_status is greater than 255. The current implementation passes such a value as is in the result, but this behavior may change in the future.

Export built-in

The export built-in exports variables to the environment.

Synopsis

export [-p] [name[=value]…]

Description

The export built-in (without the -p option) exports variables of the specified names to the environment, with optional values. If no names are given, or if the -p option is given, the names and values of all exported variables are displayed. If the -p option is given with operands, only the specified variables are displayed.

Options

The -p (--print) option causes the shell to display the names and values of all exported variables in a format that can be reused as input to restore the state of these variables. When used with operands, the option limits the output to the specified variables.

Operands

The operands are the names of variables to be exported or printed. When exporting, each name may optionally be followed by = and a value to assign to the variable.

Standard output

When exporting variables, the export built-in does not produce any output.

When printing variables, the built-in prints simple commands that invoke the export built-in to reexport the variables with the same values. Note that the commands do not include options to restore the attributes of the variables, such as the -r option to make variables read-only.

For array variables, the export built-in invocation is preceded by a separate assignment command since the export built-in does not support assigning values to array variables.

Errors

When exporting a variable with a value, it is an error if the variable is read-only.

When printing variables, it is an error if an operand names a non-existing variable.

Exit status

Zero unless an error occurs.

Examples

See Environment variables.

Compatibility

This built-in is part of the POSIX standard. Printing variables is portable only when the -p option is used without operands.

Previous versions of yash supported the non-standard -r and -X options, but these are not yet supported in yash-rs.

False built-in

The false built-in does nothing, unsuccessfully.

Synopsis

false

Description

The false built-in does nothing and returns a non-zero exit status.

Options

None.

Operands

None.

Errors

None.

In the future, the built-in may detect unexpected options or operands.

Exit Status

1.

Examples

See And-or lists for examples of using false in and-or lists. The examples of the getopts built-in also use false to indicate that an option is not specified.

Compatibility

The false utility is specified by POSIX.1-2024.

POSIX allows false to return any non-zero exit status, but most implementations return 1.

Most implementations ignore any arguments, but some implementations may respond to them. For example, the GNU coreutils implementation accepts the --help and --version options. For maximum portability, avoid passing arguments to false.

Fg built-in

The fg resumes a suspended job in the foreground.

Synopsis

fg [job_id]

Description

See Job control for an overview of job control in yash-rs. The built-in brings the specified job to the foreground and resumes its execution by sending the SIGCONT signal to it. The built-in then waits for the job to finish (or suspend again).

If the resumed job finishes, it is removed from the job list. If the job gets suspended again, it is set as the current job.

Options

None.

Operands

Operand job_id specifies which job to resume. See Job IDs for the operand format. If omitted, the built-in resumes the current job.

Standard output

The built-in writes the selected job name to the standard output.

Errors

It is an error if:

  • the specified job is not found,
  • the specified job is not job-controlled, that is, job control was off when the job was started, or
  • job control is off in the current shell environment.

Exit status

The built-in returns with the exit status of the resumed job. If the job is suspended, the exit status is as if the job had been terminated with the signal that suspended it. (See also Suspending foreground jobs.)

On error, it returns a non-zero exit status.

Examples

See Job control.

Compatibility

Many implementations allow omitting the leading % from job IDs and specifying multiple job IDs at once, though this is not required by POSIX and not yet supported in yash-rs.

Getopts built-in

The getopts built-in is used to parse options in shell scripts.

Synopsis

getopts option_spec variable_name [argument…]

Description

The getopts built-in parses single-character options in the specified arguments according to the specified option specification, and assigns the parsed options to the specified variable. This built-in is meant to be used in the condition of a while loop to iterate over the options in the arguments. Every invocation of the built-in parses the next option in the arguments. The built-in returns a non-zero exit status when there are no more options to parse.

The shell uses the OPTIND variable to keep track of the current position in the arguments. When the shell starts, the variable is initialized to 1. The built-in updates the variable to the index of the next argument to parse. When all arguments are parsed, the built-in sets the variable to the index of the first operand after the options, or to the number of arguments plus one if there are no operands.

When the built-in parses an option, it sets the specified variable to the option name. If the option takes an argument, the built-in also sets the OPTARG variable to the argument.

If the built-in encounters an option that is not listed in the option specification, the specified variable is set to ?. Additionally, if the option specification starts with a colon (:), the built-in sets the OPTARG variable to the encountered option character. Otherwise, the built-in unsets the OPTARG variable and prints an error message to the standard error describing the invalid option.

If the built-in encounters an option that takes an argument but the argument is missing, the error handling is similar to the case of an invalid option. If the option specification starts with a colon, the built-in sets the the specified variable to : (not ?) and sets the OPTARG variable to the option character. Otherwise, the built-in sets the specified variable to ?, unsets the OPTARG variable, and prints an error message to the standard error describing the missing argument.

When there are no more options to parse, the specified variable is set to ? and the OPTARG variable is unset.

In repeated invocations of the built-in, you must pass the same arguments to the built-in. You must not modify the OPTIND variable between invocations, either. Otherwise, the built-in may not be able to parse the options correctly.

To start parsing a new set of options, you must reset the OPTIND variable to 1 before invoking the built-in.

Options

None.

Operands

The first operand (option_spec) is the option specification. It is a string that contains the option characters the built-in parses. If a character is followed by a colon (:), the option takes an argument. If the option specification starts with a colon, the built-in does not print an error message when it encounters an invalid option or an option that is missing an argument.

The second operand (variable_name) is the name of the variable to which the built-in assigns the parsed option. In case of an invalid option or an option that is missing an argument, the built-in assigns ? or : to the variable (see above).

The remaining operands (argument…) are the arguments to parse. If there are no operands, the built-in parses the positional parameters.

Errors

The built-in may print an error message to the standard error when it encounters an invalid option or an option that is missing an argument (see the description above). However, this is not considered an error of the built-in itself.

The following conditions are considered errors of the built-in:

  • The built-in is invoked with less than two operands.
  • The second operand is not a valid variable name.
  • OPTIND, OPTARG, or the specified variable is read-only.
  • The built-in is re-invoked with different arguments or a different value of OPTIND than the previous invocation (except when OPTIND is reset to 1).
  • The value of OPTIND is not 1 on the first invocation.

Exit status

The built-in returns an exit status of zero if it parses an option, regardless of whether the option is valid or not. When there are no more options to parse, the exit status is one.

The exit status is two on error.

Examples

In the following example, the getopts built-in parses three kinds of options (-a, -b, and -c), of which only -b takes an argument. In case of an error, the built-in prints an error message to the standard error, so the script just exits with a non-zero exit status when $opt is set to ?.

a=false c=false
while getopts ab:c opt; do
    case "$opt" in
        a) a=true ;;
        b) b="$OPTARG" ;;
        c) c=true ;;
        '?') exit 1 ;;
    esac
done
shift "$((OPTIND - 1))"

if "$a"; then printf 'The -a option was specified\n'; fi
if [ "${b+set}" ]; then printf 'The -b option was specified with argument %s\n' "$b"; fi
if "$c"; then printf 'The -c option was specified\n'; fi
printf 'The remaining operands are: %s\n' "$*"

If you prefer to print an error message yourself, put a colon at the beginning of the option specification like this:

while getopts :ab:c opt; do
    case "$opt" in
        a) a=true ;;
        b) b="$OPTARG" ;;
        c) c=true ;;
        '?') printf 'Invalid option: -%s\n' "$OPTARG" >&2; exit 1 ;;
        :) printf 'Option -%s requires an argument\n' "$OPTARG" >&2; exit 1 ;;
    esac
done

Compatibility

The getopts built-in is specified by POSIX. Only ASCII alphanumeric characters are allowed for option names, though this implementation allows any characters but :.

The current implementation considers variable names containing a = as invalid names. However, more names many be considered invalid in the future. For best forward-compatibility and portability, only use portable name characters (ASCII alphanumerics and underscore).

Although POSIX requires the built-in to support the Utility Syntax Guidelines 3 to 10, some implementations do not support the -- separator placed before operands to the built-in itself, that is, between the built-in name getopts and the first operand option_spec.

The value of the OPTIND variable is not portable until the built-in finishes parsing all options. In this implementation, the value may temporarily contain two integers separated by a colon. The first integer is the index of the next argument to parse, and the second is the index of the character in the argument to parse. Other implementations may use a different scheme. Some sets OPTIND to the index of the just-parsed argument and uses a hidden variable to keep track of the character index.

The behavior is unspecified if you modify the OPTIND variable between invocations of the built-in or to a value other than 1.

In other implementations, the exit status may be greater than two on error.

Jobs built-in

The jobs built-in reports job status.

Synopsis

jobs [-l|-p] [job_id…]

Description

See Job control for an overview of job control in yash-rs. The jobs built-in prints information about jobs the shell is currently controlling, one line for each job. The results follow the format specified by the POSIX.

When the built-in reports a finished job (either exited or signaled), it removes the job from the job list.

Format

An example output of the built-in in the default format is:

[1] - Running              cargo build
[2] + Stopped(SIGTSTP)     vim
[3]   Done                 rm -rf /tmp/foo

The first column is the job number in brackets. The second column indicates the current job (+) or the previous job (-). The third column is the job state, which can be one of:

  • Running: the job is running in the background
  • Stopped(signal): the job is stopped by signal
  • Done: the job has finished with an exit status of zero
  • Done(n): the job has finished with non-zero exit status n
  • Killed(signal): the job has been killed by signal
  • Killed(signal: core dumped): the job has been killed by signal and a core dump was produced

The last column is the command line of the job.

Options

You can use two options to change the output.

The -l (--verbose) option displays additional details by inserting the process ID before each job state. The -p (--pgid-only) option outputs only the process ID of each job. In both cases, the process ID shown is that of the main process in the job, which is also the process group ID if the job is under job control.

Operands

Each operand is parsed as a job ID that specifies which job to report. If no operands are given, the built-in prints all jobs.

Errors

If an operand does not specify a valid job, the built-in prints an error message to the standard error and returns a non-zero exit status. An ambiguous job ID (matching multiple jobs) is also an error.

Exit status

Zero if successful, non-zero if an error occurred.

Examples

Job list includes an example of using the jobs built-in to list jobs.

The built-in with different arguments:

$ vim
[1] + Stopped(SIGTSTP)     vim
$ sleep 60 && echo "1 minute elapsed!"&
[2] 38776
$ jobs
[1] + Stopped(SIGTSTP)     vim
[2] - Running              sleep 60 && echo "1 minute elapsed!"
$ jobs -l
[1] + 37424 Stopped(SIGTSTP)     vim
[2] - 38776 Running              sleep 60 && echo "1 minute elapsed!"
$ jobs -p %2
38776

Compatibility

The output format may differ between shells. Specifically:

  • For a job stopped by SIGTSTP, other shells may omit the signal name and simply print Stopped.
  • Other shells may report stopped jobs as Suspended instead of Stopped.
  • The job state format for jobs terminated by a signal is implementation-defined.
  • With the -l option, shells that manage more than one process per job may print an additional line containing the process ID and name for each process in the job.

The current implementation of this built-in removes finished jobs from the job list after reporting all jobs. This behavior should not be relied upon. The following script shows a “job not found” error in many other shells because the built-in removes the job when processing the first operand so the job is gone when the second is processed:

$ sleep 0&
$ jobs %sleep %sleep

When the built-in is used in a subshell, the built-in reports not only jobs that were started in the subshell but also jobs that were started in the parent shell. This behavior is not portable and is subject to change.

The POSIX standard only defines the -l and -p options. Previous versions of yash supported additional options, which are not yet implemented in yash-rs.

POSIX does not define the behavior when both -l and -p options are used together. In most other shells, the option specified last takes effect. In yash-rs, the -p option takes precedence over -l, but this may change in future versions. Later releases of yash-rs might instead reject conflicting options.

A portable job ID must start with a %. If an operand does not have a leading %, the built-in assumes one silently, which is not portable.

Kill built-in

The kill built-in sends a signal to processes.

Synopsis

kill [-s SIGNAL|-n SIGNAL|-SIGNAL] target…
kill -l|-v [SIGNAL|exit_status]…

Description

Without the -l or -v option, the built-in sends a signal to processes.

With the -l or -v option, the built-in lists signal names or descriptions.

Options

The -s or -n option specifies the signal to send. The signal name is case-insensitive, but must be specified without the SIG prefix. The default signal is SIGTERM. (Specifying a signal name with the SIG prefix may be allowed in the future.)

The signal may be specified as a number instead of a name. If the number is zero, the built-in does not send a signal, but instead checks whether the shell can send the signal to the target processes.

The obsolete syntax allows the signal name or number to be specified directly after the hyphen like -TERM and -15 instead of -s TERM and -n 15.

The -l option lists signal names. The names are printed one per line, without the SIG prefix.

The -v option implies and extends the -l option by displaying the signal number before each name. The output format for -v may be changed in the future to include signal descriptions as well.

Operands

Without the -l or -v option, the built-in takes one or more operands that specify the target processes. Each operand is one of the following:

  • A positive decimal integer, which should be a process ID
  • A negative decimal integer, which should be a negated process group ID
  • 0, which means the current process group
  • -1, which means all processes
  • A job ID, which means the process group of the job

With the -l or -v option, the built-in may take operands that limit the output to the specified signals. Each operand is one of the following:

  • The exit status of a process that was terminated by a signal
  • A signal number
  • A signal name without the SIG prefix

Without operands, the -l and -v options list all signals.

Errors

It is an error if:

  • The -l or -v option is not specified and no target processes are specified.
  • A specified signal is not supported by the shell.
  • A specified target process does not exist.
  • The target job specified by a job ID operand is not job-controlled, that is, job control was off when the job was started.
  • The signal cannot be sent to any of the target processes specified by an operand.
  • An operand specified with the -l or -v option does not identify a supported signal.

Exit status

The exit status is zero unless an error occurs. The exit status is zero if the signal is sent to at least one process for each operand, even if the signal cannot be sent to some of the processes.

Usage notes

When a target is specified as a job ID, the built-in cannot tell whether the job process group still exists. If the job process group has been terminated and another process group has been created with the same process group ID, the built-in will send the signal to the new process group.

Examples

Sending SIGTERM to the last started asynchronous command and showing the signal name represented by the exit status:

$ sleep 10&
$ kill $!
$ wait $!
$ echo "Exit status $? corresponds to SIG$(kill -l $?)"
Exit status 399 corresponds to SIGTERM

Specifying a signal name and job ID:

$ set -m
$ sleep 10&
$ kill -s INT %1
$ wait %1
$ echo "Exit status $? corresponds to SIG$(kill -l $?)"
Exit status 386 corresponds to SIGINT

Specifying a signal number:

$ sleep 10&
$ kill -n 1 $!
$ wait $!
$ echo "Exit status $? corresponds to SIG$(kill -l $?)"
Exit status 385 corresponds to SIGHUP

The -- separator is needed if the first operand starts with a hyphen (a negated process group ID):

$ set -m
$ sleep 10&
$ kill -n 15 -- -$!
$ wait $!
$ echo "Exit status $? corresponds to SIG$(kill -l $?)"
Exit status 399 corresponds to SIGTERM

Compatibility

The kill built-in is specified by POSIX.1-2024.

Specifying a signal number other than 0 to the -s option is a non-standard extension.

Specifying a signal number to the -n option is a ksh extension. This implementation also supports the -n option with a signal name.

The kill -SIGNAL target… form may not be parsed as expected by other implementations when the signal name starts with an s. For example, kill -stop 123 may try to send the SIGTOP signal instead of the SIGSTOP signal.

POSIX defines the following signal numbers:

  • 0 (a dummy signal that can be used to check whether the shell can send a signal to a process)
  • 1 (SIGHUP)
  • 2 (SIGINT)
  • 3 (SIGQUIT)
  • 6 (SIGABRT)
  • 9 (SIGKILL)
  • 14 (SIGALRM)
  • 15 (SIGTERM)

Other signal numbers are implementation-defined.

Using the -l option with more than one operand is a non-standard extension. Specifying a signal name operand to the -l option is a non-standard extension.

The -v option is a non-standard extension.

Some implementations print 0 or EXIT for kill -l 0 or kill -l EXIT while this implementation regards them as invalid operands.

On some systems, a signal may have more than one name. There seems to be no consensus whether kill -l should print all names or just one name for each signal. This implementation currently prints all names, but this behavior may change in the future.

Pwd built-in

The pwd built-in prints the working directory path.

Synopsis

pwd [-L|-P]

Description

The built-in prints the pathname of the current working directory followed by a newline to the standard output.

Options

With the -L (--logical) option, the printed path is the value of the PWD variable if it is correct. The path may contain symbolic link components, but not . or .. components.

With the -P (--physical) option (or if PWD is not correct), the built-in recomputes and prints the actual path to the working directory. The output excludes symbolic link components as well as . and .. components.

These two options are mutually exclusive. The last specified one applies if given both. The default is -L.

Operands

None.

Errors

This built-in may fail for various reasons. For example:

  • The working directory has been removed from the file system.
  • You lack permission to access one or more ancestor directories required to determine the working directory’s path.
  • The standard output is not writable.

Exit Status

Zero if the path was successfully printed; non-zero otherwise.

Compatibility

POSIX-1.2024 defines the pwd utility with the -L and -P options.

POSIX allows pwd to apply the -P option if the -L option is specified and PWD is longer than PATH_MAX.

The shell sets PWD on the startup and modifies it in the cd built-in. If PWD is modified or unset otherwise, the behavior of cd and pwd is unspecified.

Read built-in

The read built-in reads a line into variables.

Synopsis

read [-d delimiter] [-r] variable…

Description

The read built-in reads a line from the standard input and assigns it to the variables named by the operands. Field splitting is performed on the line read to produce as many fields as there are variables. If there are fewer fields than variables, the remaining variables are set to empty strings. If there are more fields than variables, the last variable receives all remaining fields, including the field separators, but not trailing whitespace separators.

Non-default delimiters

By default, the built-in reads a line up to a newline character. The -d option changes the delimiter to the character specified by the delimiter value. If the delimiter value is empty, the built-in reads a line up to the first nul byte.

Escaping

By default, backslashes in the input are treated as quoting characters that prevent the following character from being interpreted as a field separator. Backslash-newline pairs are treated as line continuations.

The -r option disables this behavior.

Prompting

By default, the built-in does not display a prompt before reading a line.

When reading lines after the first line, the built-in displays the value of the PS2 variable as a prompt if the shell is interactive and the input is from a terminal. See Command prompt for details.

Options

The -d (--delimiter) option takes an argument and changes the delimiter to the character specified by the argument. If the delimiter value is empty, the read built-in reads a line up to the first nul byte. Multibyte characters are not supported.

The -r (--raw-mode) option disables the interpretation of backslashes.

Operands

One or more operands are required. Each operand is the name of a variable to be assigned.

Errors

This built-in fails if:

  • The standard input is not readable.
  • The delimiter is not a single-byte character.
  • The delimiter is not a nul byte and the input contains a nul byte.
  • A variable name is not valid.
  • A variable to be assigned is read-only.

Exit status

The exit status is zero if a line was read successfully and non-zero otherwise. If the built-in reaches the end of the input before finding a delimiter, the exit status is one, but the variables are still assigned with the line read so far. On other errors, the exit status is two or higher.

Examples

Reading a line from a file:

$ mkdir $$ && cd $$ || exit
$ cat > users.txt <<END
> 1 James Carter
> 2 Emily Johnson
> 3 Michael Anthony Davis
> END
$ read id fullname < users.txt
$ echo "ID: $id, Full Name: $fullname"
ID: 1, Full Name: James Carter

Reading all lines from a file:

$ mkdir $$ && cd $$ || exit
$ cat > users.txt <<END
> 1 James Carter
> 2 Emily Johnson
> 3 Michael Anthony Davis
> END
$ while read id fullname; do
>     echo "ID: $id, Full Name: $fullname"
> done < users.txt
ID: 1, Full Name: James Carter
ID: 2, Full Name: Emily Johnson
ID: 3, Full Name: Michael Anthony Davis

Note that the < users.txt redirection must be applied to the while loop, not to the read command. See redirection Semantic details for why.

Using a non-default field separator:

$ mkdir $$ && cd $$ || exit
$ cat > users.txt <<END
> 1:James Carter
> 2:Emily Johnson
> 3:Michael Anthony Davis
> END
$ while IFS=: read id fullname; do
>     echo "ID: $id, Full Name: $fullname"
> done < users.txt
ID: 1, Full Name: James Carter
ID: 2, Full Name: Emily Johnson
ID: 3, Full Name: Michael Anthony Davis

Reading a nul-terminated string:

$ mkdir $$ && cd $$ || exit
$ echo "Hello, world!" > foo.txt
$ find . -type f -print0 |
> while read -d '' file; do
>     echo "File ${file#./} contains:"
>     cat "$file"
> done
File foo.txt contains:
Hello, world!

Use the -r option and an empty IFS to read a line literally:

$ mkdir $$ && cd $$ || exit
$ echo ' No field splitting.  Nor line continuation. \' > line.txt
$ IFS= read -r line < line.txt
$ printf '[%s]\n' "$line"
[ No field splitting.  Nor line continuation. \]

Compatibility

POSIX.1-2024 defines the read built-in with the -d and -r options. Previous versions of yash supported additional options, which are not yet implemented in yash-rs.

In this implementation, a line continuation is always a backslash followed by a newline. Other implementations may allow a backslash followed by a delimiter to be a line continuation if the delimiter is not a newline.

When a backslash is specified as the delimiter, no escape sequences are recognized. Other implementations may recognize escape sequences in the input line, effectively never recognizing the delimiter.

In this implementation, the value of the PS2 variable is subject to parameter expansion, command substitution, and arithmetic expansion. Other implementations may not perform these expansions.

The current implementation considers variable names containing a = as invalid names. However, more names may be considered invalid in the future. For best forward-compatibility and portability, only use portable name characters (ASCII alphanumerics and underscore).

In yash-rs and many other implementations, the read built-in does not read more than needed to find a delimiter, so that a next command can read the remaining input without loss. POSIX.1-2024 requires this behavior only when the built-in is reading from a seekable input.

Readonly built-in

The readonly built-in provides several operations related to read-only variables:

Making variables read-only

If the -p (--print) option is not specified and there are any operands, the built-in makes the specified variables read-only.

Synopsis

readonly name[=value]…

Options

None.

Operands

Operands specify the names and values of the variables to be made read-only. If an operand contains an equal sign (=), the operand is split into the name and value at the first equal sign. The value is assigned to the variable named by the name. Otherwise, the variable named by the operand is created without a value unless it is already defined, in which case the existing value is retained.

If no operands are given, the built-in prints variables (see below).

Standard output

None.

Printing read-only variables

If the -p (--print) option is specified, the built-in prints the names and values of the variables named by the operands in the format that can be evaluated as shell code to recreate the variables. If there are no operands, the built-in prints all read-only variables in the same format.

Synopsis

readonly -p [name…]
readonly

Options

The -p (--print) option must be specified to print variables unless there are no operands.

Operands

Operands specify the names of the variables to be printed. If no operands are given, all read-only variables are printed.

Standard output

A command string that invokes the readonly built-in to recreate the variable is printed for each read-only variable. Note that the command does not include options to restore the attributes of the variable, such as the -x option to make variables exported.

Also note that evaluating the printed commands in the current shell session will fail (unless the variable is declared without a value) because the variable is already defined and read-only.

For array variables, the built-in invocation is preceded by a separate assignment command since the built-in does not support assigning values to array variables.

Errors

When making a variable read-only with a value, it is an error if the variable is already read-only.

When printing variables, it is an error if an operand names a non-existing variable.

Exit status

Zero if successful, non-zero if an error occurred.

Examples

$ readonly foo='Hello, world!'
$ echo "$foo"
Hello, world!
$ readonly
readonly foo='Hello, world!'
$ foo='Goodbye, world!'
error: error assigning to variable
 --> <stdin>:4:1
  |
4 | foo='Goodbye, world!'
  | ^^^^^^^^^^^^^^^^^^^^^ cannot assign to read-only variable "foo"
  |
 ::: <stdin>:1:10
  |
1 | readonly foo='Hello, world!'
  |          ------------------- info: the variable was made read-only here
  |

Compatibility

This built-in is part of the POSIX standard. Printing variables is portable only when the -p option is used without operands.

Return built-in

The return built-in quits the currently executing innermost function or script.

Synopsis

return [-n] [exit_status]

Description

return exit_status makes the shell return from the currently executing function or script with the specified exit status.

If the shell is not executing a function or script, the built-in will work like exit, but this behavior may change in the future.

Options

The -n (--no-return) option makes the built-in not actually quit a function or script. This option will be helpful when you want to set the exit status to an arbitrary value without any other side effect.

Operands

The optional exit_status operand, if given, should be a non-negative decimal integer and will be the exit status of the built-in.

Errors

If the exit_status operand is given but not a valid non-negative integer, it is a syntax error.

This implementation treats an exit_status value greater than 2147483647 as a syntax error.

Exit status

The exit_status operand will be the exit status of the built-in.

If the operand is not given, the exit status will be the current exit status ($?). If the built-in is invoked in a trap executed in a function or script and the built-in returns from that function or script, the exit status will be the value of $? before entering the trap.

In case of an error, the exit status is 2.

Examples

See Returning from functions.

Compatibility

The return built-in is specified by POSIX.1-2024.

POSIX only requires the return built-in to quit a function or sourced script. The behavior for other kinds of scripts is a non-standard extension.

In some shells, the return built-in lacks support for the -- separator.

The -n (--no-return) option is a non-standard extension.

The behavior is unspecified in POSIX if exit_status is greater than 255. The current implementation passes such a value as is in the result, but this behavior may change in the future.

Set built-in

The set built-in modifies shell options and positional parameters. It also can print a list of current options or variables.

Description

The built-in behaves differently depending on the invocation syntax.

Printing variables

When executed without any arguments, the built-in prints a list of variables visible in the current execution environment. The list is formatted as a sequence of simple commands performing an assignment that would restore the present variables if executed (unless the assignment fails because of a read-only variable). The list is ordered alphabetically.

$ set
HOME=/home/alice
OPTIND=1
LOGNAME=alice
PATH=/usr/local/bin:/usr/bin:/bin
PWD=/home/alice/my_project
TERM=xterm

Some built-ins allow creating variables that do not have a valid name. For example, export 1a=foo defines a variable named 1a, which cannot be assigned to with the normal assignment syntax of the simple command. Such variables are not printed by the set built-in.

Printing shell options

If you specify the -o option as a unique argument to the set built-in, it prints the current shell option settings in a human-readable format:

$ set -o
allexport        off
clobber          on
cmdline          off
errexit          off
exec             on
...

If you use the +o option instead, the printing lists shell commands that would restore the current option settings if executed:

$ set +o
set +o allexport
set -o clobber
#set +o cmdline
set +o errexit
set -o exec
...

Modifying shell options

Other command line options modify shell option settings. They can be specified in the short form like -e or the long form like -o errexit and --errexit.

You can also specify options starting with + in place of -, as in +e, +o errexit, and ++errexit. The - options turn on the corresponding shell options while the + options turn off.

See Enabling and disabling options for the full details on the option syntax. Available options are listed in the Option list.

You cannot modify the following options with the set built-in:

  • cmdline (-c)
  • interactive (-i)
  • stdin (-s)

Modifying positional parameters

If you specify one or more operands, they will be new positional parameters in the current shell environment, replacing any existing positional parameters.

See Modifying positional parameters for examples of how to set positional parameters.

Option-operand separator

As with other utilities conforming to POSIX XBD Utility Syntax Guidelines, the set built-in accepts -- as a separator between options and operands. Additionally, yash-rs also accepts - as a separator.

$ set -o errexit -- foo bar
$ echo "$1" "$2"
foo bar

If you place a separator without any operands, the built-in will clear all positional parameters.

$ set --
$ echo $#
0

Exit status

  • 0: successful
  • 1: error printing output
  • 2: invalid options

Compatibility

The set built-in is specified by POSIX.1-2024. See Compatibility for the compatibility of the option syntax and available options.

The output format of set -o and set +o depends on the shell.

The semantics of - as an option-operand separator is unspecified in POSIX. You should prefer --. Older versions of yash treated - simply as an operand.

Many (but not all) shells specially treat +, especially when it appears in place of an option-operand separator. Yash does not treat + specially, so it can be used as an operand without another separator.

Other implementations may return different non-zero exit statuses for errors.

Shift built-in

The shift built-in removes some positional parameters.

Synopsis

shift [n]

Description

The built-in removes the first n positional parameters from the list of positional parameters. If n is omitted, it is assumed to be 1.

Options

None.

Operands

The operand specifies the number of positional parameters to remove. It must be a non-negative decimal integer less than or equal to the number of positional parameters.

Errors

It is an error to try to remove more than the number of existing positional parameters.

Exit status

Zero if successful, non-zero if an error occurred.

Examples

Modifying positional parameters includes an example of the shift built-in.

Compatibility

The shift built-in is part of POSIX.1-2024.

In some shells, the shift built-in lacks support for the -- separator.

Source (.) built-in

The source (.) built-in reads and executes commands from a file.

Synopsis

. file [arguments…]
source file [arguments…]

Description

The . built-in reads and executes commands from the specified file in the current shell environment.

If the filename does not contain a slash, the shell searches the directories in the PATH variable for the file. The search is similar to command search, but the file does not need to be executable; any readable file found first is used.

Options

None.

Operands

The first operand file must be given and is the pathname of the file to be executed. If it does not contain a slash, it is subject to the search described above.

Any additional arguments are currently ignored. Future versions may support assigning these to the positional parameters.

Errors

It is an error if the file cannot be found or read. During parsing and execution, any syntax error or runtime error may occur.

Exit status

The exit status of the built-in is the exit status of the last command executed in the file. If there is no command in the file, the exit status is zero.

If the file cannot be found or read, the exit status is 1. In case of a syntax error in the file, the exit status is 2.

Examples

See Reading and executing files.

Compatibility

The . built-in is specified in the POSIX standard. The built-in name source is a non-standard extension that is also available in some other shells.

POSIX defines no options for the . built-in, but previous versions of yash supported additional options, which are not yet implemented in yash-rs.

If the pathname of the file does not contain a slash and the file is not found in the command search, some shells may fall back to the file in the current working directory. This is a non-portable extension that is not specified in POSIX. The portable way to specify a file in the current working directory is to prefix the filename with ./ as in . ./foo.sh.

Setting the positional parameters with additional operands is a non-standard extension that is supported by some other shells. The behavior about the local variable context may differ in other shells.

Other implementations may return a different non-zero exit status for an error.

According to POSIX.1-2024, “An unrecoverable read error while reading from the file operand of the dot special built-in shall be treated as a special built-in utility error.” This means that if you use the . built-in through the command built-in and an unrecoverable read error occurs, the shell should not exit immediately. However, yash-rs does not currently support this behavior: if an unrecoverable read error happens and the shell is not running interactively, yash-rs will always exit. See Shell errors for the conditions under which the shell should exit.

Times built-in

The times built-in is used to display the accumulated user and system times for the shell and its children.

Synopsis

times

Description

The built-in prints the accumulated user and system times for the shell and its children.

Options

None.

Operands

None.

Standard output

Two lines are printed to the standard output, each in the following format:

1m2.345678s 3m4.567890s

The first field of each line is the user time, and the second field is the system time. The first line shows the times consumed by the shell itself, and the second line shows the times consumed by its child processes.

Errors

It is an error if the times cannot be obtained or the standard output is not writable.

Exit status

Zero unless an error occurred.

Compatibility

The times built-in is defined in POSIX.

POSIX requires each field to be printed with six digits after the decimal point, but many implementations print less. Note that the number of digits does not necessarily indicate the precision of the times.

Trap built-in

The trap built-in sets or prints traps.

Synopsis

trap [action] condition…
trap [-p [condition…]]

Description

The trap built-in can be used to either set or print traps. To set traps, pass an action and one or more conditions as operands. To print the currently configured traps, invoke the built-in with no operands or with the -p option.

Setting traps

When setting traps, the built-in sets the action for each condition in the current shell environment. To set different actions for multiple conditions, invoke the built-in for each action.

Printing traps

When the built-in is invoked with no operands, it prints the currently configured traps in the format trap -- action condition where action and condition are properly quoted so that the output can be read by the shell to restore the traps. By default, the built-in prints traps that have non-default actions. To print all traps, use the -p option with no operands.

When the -p option is used with one or more conditions, the built-in prints the traps for the specified conditions.

When a subshell is entered, traps with a custom action are reset to the default action. This behavior would make it impossible to save the current traps by using a command substitution as in traps=$(trap). To make this work, when the built-in is invoked in a subshell and no traps have been modified in the subshell, it prints the traps that were configured in the parent shell.

Options

The -p (--print) option prints the traps configured in the shell environment.

Operands

An action specifies what to do when the trap condition is met. It may be one of the following:

  • - (hyphen) resets the trap to the default action.
  • An empty string ignores the trap.
  • Any other string is treated as a command to execute.

The action may be omitted if the first condition is a non-negative decimal integer. In this case, the built-in resets the trap to the default action.

A condition specifies when the action is triggered. It may be one of the following:

  • A symbolic name of a signal without the SIG prefix (e.g. INT, QUIT, TERM)
    • Signal names must be specified in uppercase. Lowercase names and the SIG prefix may be supported in the future.
  • A positive decimal integer representing a signal number
  • The number 0 or the symbolic name EXIT representing the termination of the main shell process
    • This condition is not triggered when the shell exits due to a signal.

Errors

Traps cannot be set to SIGKILL or SIGSTOP.

Invalid conditions are reported with a non-zero exit status, but it is not a shell error.

If a non-interactive shell inherits a signal disposition that ignores a signal, it does not permit modifying the trap for that signal. In yash-rs, this restriction is enforced silently and does not affect the exit status of the built-in.

Exit status

Zero if successful, non-zero if an error is reported.

Examples

Setting a trap and reacting to a self-sent signal:

$ trap 'echo "Caught SIGINT"; exit' INT
$ kill -s INT $$
Caught SIGINT

Ignoring a signal:

$ trap '' INT
$ kill -s INT $$
$ echo "The shell survived the INT signal."
The shell survived the INT signal.

Showing traps:

$ trap '' INT QUIT; trap 'echo "Caught SIGTERM"' TERM
$ trap
trap -- '' INT
trap -- '' QUIT
trap -- 'echo "Caught SIGTERM"' TERM
$ trap -p INT
trap -- '' INT

Compatibility

The trap built-in is specified by POSIX.1-2024.

Portable scripts should specify signals in uppercase letters without the SIG prefix. Specifying signals by numbers is discouraged as signal numbers vary among systems.

The result of setting a trap to SIGKILL or SIGSTOP is undefined by POSIX.

The mechanism for the built-in to print traps configured in the parent shell may vary among shells. This implementation remembers the old traps internally when starting a subshell and prints them when the built-in is invoked in the subshell. POSIX allows another scheme: When starting a subshell, the shell checks if the subshell command contains only a single invocation of the trap built-in, in which case the shell skips resetting traps on the subshell entry so that the built-in can print the traps configured in the parent shell. The check may be done by a simple literal comparison, so you should not expect the shell to recognize complex expressions such as cmd=trap; traps=$($cmd).

In other shells, the EXIT condition may be triggered when the shell is terminated by a signal.

True built-in

The true built-in does nothing, successfully.

Synopsis

true

Description

The true built-in does nothing, successfully. It is useful as a placeholder when a command is required but no action is needed.

Options

None.

Operands

None.

Errors

None.

In the future, the built-in may detect unexpected options or operands.

Exit Status

Zero.

Examples

See And-or lists for examples of using true in and-or lists. The examples of the getopts built-in also use true to indicate that an option is specified.

Compatibility

The true utility is specified by POSIX.1-2024.

Most implementations ignore any arguments, but some implementations respond to them. For example, the GNU coreutils implementation accepts the --help and --version options. For maximum portability, avoid passing arguments to true. To pass and ignore arguments, use the : (colon) built-in instead.

Type built-in

The type built-in identifies the type of commands.

Synopsis

type [name…]

Description

The type built-in prints the description of the specified command names.

Options

None.

Operands

The name operands specify the command names to identify.

Standard output

The command descriptions are printed to the standard output.

Errors

It is an error if the name is not found.

Exit status

The exit status is zero if all the names are found, and non-zero otherwise.

Examples

$ PATH=/usr/bin:/bin
$ alias ll='ls -l'
$ greet() { echo "Hello, world!"; }
$ type ll greet cd env
ll: alias for `ls -l`
greet: function
cd: mandatory built-in
env: external utility at /usr/bin/env

Compatibility

The type built-in is specified by POSIX.1-2024.

POSIX defines no options for the type built-in, but previous versions of yash supported additional options, which are not yet implemented in yash-rs.

In dash, the -- separator is treated as an operand.

POSIX requires that the name operand be specified, but many implementations allow it to be omitted, in which case the built-in does nothing.

The format of the output is unspecified by POSIX. In this implementation, the type built-in is equivalent to the command built-in with the -V option.

Typeset built-in

The typeset built-in provides several operations related to variables and functions:

Defining variables

If neither the -p (--print) nor the -f (--functions) option is specified and there are operands, the built-in defines variables named by the operands.

You can specify additional options to set the scope and attributes of the variables.

Synopsis

typeset [-grx] [+rx] name[=value]…

Options

By default, the built-in creates or updates variables locally within the current function. If the -g (--global) option is specified, the built-in affects existing variables visible in the current scope (which may be outside the current function) or creates new variables globally.

The following options set variable attributes:

  • -r (--readonly): Makes the variables read-only.
  • -x (--export): Exports the variables to the environment.

To remove these attributes, use the corresponding option with a plus sign (+) instead of a minus sign (-). For example, the following commands stop exporting the variable foo:

typeset +x foo
typeset ++export foo

Note: The read-only attribute cannot be removed. Using the +r option to read-only variables causes an error.

Operands

Operands specify the names and values of the variables to define. If an operand contains an equal sign (=), it is split at the first equal sign into a name and a value. The value is assigned to the variable with that name. If an operand does not contain an equal sign, the variable is created without a value unless it already exists, in which case its value is retained.

If no operands are given, the built-in prints variables (see below).

Standard output

None.

Examples

See Local variables for examples of defining local variables.

The following example demonstrates defining a local read-only variable:

$ greet() {
>     typeset -r user=Alice
>     echo "Hello, $user!"
> }
$ greet
Hello, Alice!
$ user=Bob
$ echo "Now the user is $user."
Now the user is Bob.

Printing variables

If the -p (--print) option is specified and the -f (--functions) option is not, the built-in prints the attributes and values of the variables named by the operands, using a format that can be evaluated as shell code to recreate the variables. If there are no operands and the -f (--functions) option is not specified, the built-in prints all variables in the same format, in alphabetical order.

Synopsis

typeset -p [-grx] [+rx] [name…]
typeset [-grx] [+rx]

Options

The -p (--print) option must be specified to print variables when operands are given. Otherwise, the built-in defines variables. If there are no operands, the option may be omitted.

By default, the built-in prints variables in the current context. If the -g (--global) option is specified, it prints variables visible in the current scope (which may be outside the current function).

The following options filter which variables are printed. Variables that do not match the criteria are ignored.

If these options are negated by using a plus sign (+) instead of a minus sign (-), the built-in prints variables that do not have the corresponding attribute. For example, the following command shows all non-exported variables:

typeset -p +x

Operands

Operands specify the names of the variables to print. If no operands are given, the built-in prints all variables that match the selection criteria.

Standard output

For each variable, a command string that invokes the typeset built-in to recreate the variable is printed. For array variables, a separate assignment command precedes the typeset command, since the built-in does not support assigning values to arrays. In this case, the typeset command is omitted if no options are applied to the variable.

Note: Evaluating the printed commands in the current context may fail if variables are read-only, since read-only variables cannot be assigned values.

Examples

$ foo='some value that contains spaces'
$ bar=(this is a readonly array)
$ typeset -r bar
$ typeset -p foo bar
typeset foo='some value that contains spaces'
bar=(this is a readonly array)
typeset -r bar

Modifying function attributes

If the -f (--functions) option is specified, the -p (--print) option is not specified, and there are operands, the built-in modifies the attributes of functions named by the operands.

Synopsis

typeset -f [-r] [+r] name…

Options

The -f (--functions) option is required to modify functions. Otherwise, the built-in defines variables.

The -r (--readonly) option makes the functions read-only. If this option is not specified, the built-in does nothing.

The built-in accepts the +r (++readonly) option, but it has no effect since the read-only attribute cannot be removed.

Operands

Operands specify the names of the functions to modify. If no operands are given, the built-in prints functions (see below).

Note: The built-in operates only on existing functions. It cannot create new functions or change the body of existing functions.

Standard output

None.

Examples

See Read-only functions.

Printing functions

If both the -f (--functions) and -p (--print) options are specified, the built-in prints the attributes and definitions of the functions named by the operands, using a format that can be evaluated as shell code to recreate the functions. If there are no operands and the -f (--functions) option is specified, the built-in prints all functions in the same format, in alphabetical order.

Synopsis

typeset -fp [-r] [+r] [name…]
typeset -f [-r] [+r]

Options

The -f (--functions) and -p (--print) options must both be specified to print functions when operands are given. Otherwise, the built-in modifies functions. The -p (--print) option may be omitted if there are no operands.

The -r (--readonly) option can be specified to limit the output to read-only functions. If this option is negated as +r (++readonly), the built-in prints functions that are not read-only. If the option is not specified, all functions are printed.

Operands

Operands specify the names of the functions to print. If no operands are given, the built-in prints all functions that match the selection criteria.

Standard output

For each function, a function definition command is printed, which may be followed by a typeset command to set the function’s attributes. The output is formatted by the shell and may differ from the original function definition.

Note: Evaluating the printed commands in the current shell environment may fail if functions are read-only, since read-only functions cannot be redefined.

Currently, yash-rs does not print the contents of here-documents. Functions containing here-documents are not correctly recreated when the output is evaluated.

Examples

See Showing function definitions.

Errors

The read-only attribute cannot be removed from a variable or function. If a variable is already read-only, you cannot assign a value to it.

It is an error to modify a non-existent function.

When printing variables or functions, it is an error if an operand names a non-existent variable or function.

Exit status

Zero if successful; non-zero if an error occurs.

Additional notes

The -g (--global) option has no effect if the built-in is used outside a function.

Compatibility

The typeset built-in is not specified by POSIX, and many shells implement it differently. This implementation is based on common characteristics found in other shells, but it is not fully compatible with any of them.

Some implementations allow operating on variables and functions at the same time. This implementation does not.

This implementation requires the -g (--global) option to print variables defined outside the current function. Other implementations may print such variables by default.

This implementation allows hiding a read-only variable defined outside the current function by introducing a variable with the same name in the current function. Other implementations may not allow this.

Historical versions of yash performed assignments when operands of the form name=value were given, even if the -p option was specified. This implementation treats such usage as an error.

Historical versions of yash used the -X (--unexport) option to negate the -x (--export) option. This is now deprecated because its behavior was incompatible with other implementations. Use the +x (++export) option instead.

Ulimit built-in

The ulimit built-in displays or sets system resource limits for the current shell process.

Synopsis

ulimit [-SH] [-a|-b|-c|-d|-e|-f|-i|-k|-l|-m|-n|-q|-R|-r|-s|-t|-u|-v|-w|-x] [limit]

Description

The ulimit built-in allows you to view or change resource limits imposed by the operating system on the shell and its child processes. The limit operand specifies the new value for a resource limit. If limit is omitted, the current value is displayed.

Each resource has two limits:

  • Soft limit: The value enforced by the kernel for the process. It can be increased up to the hard limit.
  • Hard limit: The maximum value to which the soft limit can be set. Any process can lower its hard limit, but only privileged processes can raise it.

Options

  • -S (--soft): Set or display the soft limit.
  • -H (--hard): Set or display the hard limit.

If neither -S nor -H is specified:

  • When setting a limit, both soft and hard limits are changed.
  • When displaying a limit, only the soft limit is shown.

Specifying both -S and -H together sets both limits. However, when displaying limits, using both options is an error.

Resource selection

Specify a resource to set or display using one of the following options. Supported resources may vary by platform, so not all options are available everywhere:

  • -b (--sbsize): maximum size of the socket buffer (bytes)
  • -c (--core): maximum size of a core file created by a terminated process (512-byte blocks)
  • -d (--data): maximum size of a data segment of the process (kilobytes)
  • -e (--nice): maximum process priority (see below)
  • -f (--fsize): maximum size of a file the process can create (512-byte blocks)
  • -i (--sigpending): maximum number of signals that can be queued to the process
  • -k (--kqueues): maximum number of kernel event queues
  • -l (--memlock): maximum size of memory locked into RAM (kilobytes)
  • -m (--rss): maximum physical memory size of the process (kilobytes)
  • -n (--nofile): maximum number of open files in the process
  • -q (--msgqueue): maximum total size of POSIX message queues
  • -R (--rttime): maximum amount of CPU time the process can consume in real-time scheduling mode without a blocking system call (microseconds)
  • -r (--rtprio): maximum real-time priority
  • -s (--stack): maximum size of the process’s stack (kilobytes)
  • -t (--cpu): maximum amount of CPU time the process can consume (seconds)
  • -u (--nproc): maximum number of processes the user can run
  • -v (--as): maximum total memory size of the process (kilobytes)
  • -w (--swap): maximum size of the swap space the user can occupy (kilobytes)
  • -x (--locks): maximum number of file locks the process can hold

The limit operand and output values use the units shown in parentheses above.

If no resource option is specified, ulimit defaults to -f (--fsize).

For -e (--nice), the limit value sets the lowest nice value allowed, using the formula: nice = 20 - limit. Lower nice values mean higher priority. For example, ulimit -e 25 allows the nice value to be lowered to -5.

To display all resource limits, use -a (--all). This cannot be combined with a limit operand.

Operands

The limit operand sets a new value for the selected resource. It is interpreted as follows:

  • A non-negative integer sets the limit to that value.
  • unlimited sets the limit to the maximum allowed.
  • hard sets the limit to the current hard limit.
  • soft sets the limit to the current soft limit.

Standard output

If limit is omitted, the built-in prints the current value for the selected resource. It is either a non-negative integer or unlimited.

With -a, it prints all resource limits in a table that includes the short option name, the resource name, and the current limit value for each resource.

Errors

The built-in fails if:

  • The specified resource is unsupported on the current platform.
  • The soft limit is set higher than the hard limit.
  • The hard limit is set above the current hard limit without sufficient privileges.
  • The limit operand is out of range.
  • Both -S and -H are specified without a limit operand.
  • More than one resource option is specified.

Exit status

Zero if successful; non-zero if an error occurs.

Examples

Setting resource limits:

$ ulimit -n 64
$ ulimit -t unlimited
$ ulimit -S -v hard
$ ulimit -d hard
$ ulimit -H -d soft

Showing resource limits:

$ ulimit -n 64
$ ulimit -S -n 32
$ ulimit -H -n
64
$ ulimit -S -n
32
$ ulimit -n
32

Showing all resource limits:

$ ulimit -a
-v: virtual address space size (KiB) unlimited
-c: core dump size (512-byte blocks) 0
-t: CPU time (seconds)               unlimited
-d: data segment size (KiB)          unlimited
-f: file size (512-byte blocks)      unlimited
-x: number of file locks             unlimited
-l: locked memory size (KiB)         65536
-q: message queue size (bytes)       819200
-e: process priority (20 - nice)     0
-n: number of open files             1024
-u: number of processes              62113
-m: resident set size (KiB)          unlimited
-r: real-time priority               0
-R: real-time timeout (microseconds) unlimited
-i: number of pending signals        62113
-s: stack size (KiB)                 8192

Compatibility

The ulimit built-in is specified by POSIX.1-2024, but some behaviors are implementation-defined.

Only these options are required by POSIX: -H, -S, -a, -c, -d, -f, -n, -s, -t, and -v. Other options are extensions.

Some shells do not allow combining options (e.g., ulimit -fH). For portability, specify options separately (e.g., ulimit -f -H).

Shells differ in behavior when both -H and -S are given. Yash-rs sets or displays both limits; older versions of yash only honored the last one.

Specifying multiple resource options is an error in yash-rs, but some shells allow operating on multiple resources at once.

The hard and soft values for the limit operand are not defined by POSIX.

The output format for ulimit -a is implementation-defined and subject to change.

Umask built-in

The umask built-in shows or sets the file mode creation mask.

Synopsis

umask [-S] [mode]

Description

The built-in shows the current file mode creation mask if no mode is given. Otherwise, it sets the file mode creation mask to mode.

The file mode creation mask is a set of permissions that determines the default permissions for newly created files. In the numeric form, it is represented as a three-digit octal number, where each digit represents the permissions for the user, group, and others, respectively. For example, if the mask is set to 077, all permissions for group and others are removed, and only the user can have read, write, and execute permissions.

$ umask 077
$ mkdir foo
$ echo "Hello, world!" > greet.txt
$ ls -do foo greet.txt
drwx------ 2 user 4096 Aug 11 13:07 foo
-rw------- 1 user   14 Aug 11 13:07 greet.txt

If the mask is 000, all permissions are granted to the user, group, and others.

$ umask 000
$ mkdir foo
$ echo "Hello, world!" > greet.txt
$ ls -do foo greet.txt
drwxrwxrwx 2 user 4096 Aug 11 13:07 foo
-rw-rw-rw- 1 user   14 Aug 11 13:07 greet.txt

Note that execute permissions are not granted for the text files created by the redirections because redirection omits the execute permission when creating files. In general, the actual permissions set for files may be more restrictive than what the current mask suggests.

Options

The -S (--symbolic) option causes the built-in to show the current file mode creation mask in symbolic notation. Without this option, the mask is shown in octal notation.

Operands

mode is an octal integer or a symbolic notation that represents the file mode creation mask. The octal number is the bitwise OR of the file mode bits to be turned off when creating a file. The symbolic notation specifies the file mode bits to be kept on when creating a file.

Numeric notation

The numeric notation is a three-digit octal number, where each digit represents the permissions for the user, group, and others, respectively. Each digit can be a value from 0 to 7, which is a sum of:

  • 4 for read permission,
  • 2 for write permission,
  • 1 for execute permission.

For example, the mask 027 removes write permission for the group and all permissions for others, while granting all permissions to the user.

Symbolic notation

The symbolic notation consists of one or more clauses separated by commas. Each clause consists of a (possibly empty) sequence of who symbols followed by one or more actions. The who symbols are:

  • u for the user bits,
  • g for the group bits,
  • o for the other bits, and
  • a for all bits.

An action is an operator optionally followed by permission symbols. The operators are:

  • + to add the permission,
  • - to remove the permission, and
  • = to set the permission.

The permission symbols are:

  • one or more of:
    • r for the read permission,
    • w for the write permission,
    • x for the execute permission,
    • X for the execute permission if the execute permission is already set for any who, and
    • s for the set-user-ID-on-execution and set-group-ID-on-execution bits.
  • u for the current user permission,
  • g for the current group permission, and
  • o for the current other permission.

For example, the symbolic notation u=rwx,go+r-w:

  • sets the user bits to read, write, and execute,
  • adds the read permission to the group and other bits, and
  • removes the write permission from the group and other bits.

A symbolic mode that starts with - may be confused as an option. To avoid this, use umask -- -w or umask a-w instead of umask -w.

Standard output

If no mode is given, the built-in prints the current file mode creation mask in octal notation followed by a newline to the standard output. If the -S option is effective, the mask is formatted in symbolic notation instead.

Errors

It is an error if the specified mode is not a valid file mode creation mask.

Exit status

Zero if successful; non-zero if an error occurs.

Examples

Setting and showing the file mode creation mask:

$ umask 027
$ umask
027

Using symbolic notation:

$ umask ug=rwx,g-w,o=
$ umask -S
u=rwx,g=rx,o=

Security considerations

If the file mode creation mask is too permissive, sensitive files may become accessible to unauthorized users. It is generally recommended to remove write permissions for group and other users.

Compatibility

The umask built-in is defined in POSIX.1-2024.

POSIX does not specify the default output format used when the -S option is not given. Yash-rs, as well as many others, uses octal notation. In any cases, the output can be reused as mode to restore the previous mask.

This implementation ignores the -S option if mode is given. However, bash prints the new mask in symbolic notation if the -S option and mode are both given.

An empty sequence of who symbols is equivalent to a in this implementation as well as many others. However, this may not be strictly true to the POSIX specification.

The permission symbols other than r, w, and x are not widely supported. This implementation currently ignores the s symbol.

Unalias built-in

The unalias built-in removes alias definitions.

Synopsis

unalias name…
unalias -a

Description

The unalias built-in removes alias definitions as specified by the operands.

Options

The -a (--all) option removes all alias definitions.

Operands

Each operand must be the name of an alias to remove.

Errors

It is an error if an operand names a non-existent alias.

Exit status

Zero unless an error occurs.

Examples

$ alias greet='echo Hello,'
$ greet world
Hello, world
$ unalias greet
$ greet world
error: cannot execute external utility "greet"
 --> <stdin>:4:1
  |
4 | greet world
  | ^^^^^ utility not found
  |

Compatibility

The unalias built-in is specified in POSIX.

Some shells implement some built-in utilities as predefined aliases. Using unalias -a may make such built-ins unavailable.

Unset built-in

The unset built-in unsets variables or functions.

Synopsis

unset [-f|-v] name…

Description

The built-in unsets variables or functions named by the operands.

Options

Either of the following options may be used to select what to unset:

  • The -v (--variables) option unsets variables. This is the default behavior.
  • The -f (--functions) option unsets functions.

Operands

Operands are the names of variables or functions to unset.

Errors

Unsetting a read-only variable or function is an error.

It is not an error to unset a variable or function that is not set. The built-in ignores such operands.

Exit status

Zero if successful; non-zero if an error occurs.

Examples

See Removing variables and Removing functions.

Compatibility

The unset built-in is specified by POSIX.1-2024.

The behavior is not portable when both -f and -v are specified. Earlier versions of yash used to honor the last specified option, but this version errors out.

If neither -f nor -v is specified and the variable named by an operand is not set, POSIX allows the built-in to unset the same-named function if it exists. Yash does not do this.

POSIX requires that at least one operand be specified. Yash-rs currently does not detect this and allows the built-in to be called without any operands, but this may change in the future.

When a global variable is hidden by a local variable, the current implementation unsets the both. This is not portable. Old versions of yash used to unset the local variable only.

Wait built-in

The wait built-in waits for jobs to finish.

Synopsis

wait [job_id_or_process_id…]

Description

See Job control for an overview of job control in yash-rs, though this built-in can also be used for asynchronous commands without job control. If you specify one or more operands, the built-in waits for the specified jobs to finish. Otherwise, the built-in waits for all existing jobs. If the jobs are already finished, the built-in returns without waiting.

If you attempt to wait for a suspended job, the built-in will wait indefinitely until the job is resumed and completes. Currently, there is no way to cancel a wait in progress. When job control is enabled, it is often preferable to use fg instead of wait, as fg allows you to interact with the job—including suspending or interrupting it.

In the future, the shell may provide a way to cancel a wait in progress or treat a suspended job as if it were finished.

Options

None.

Operands

An operand can be a job ID or decimal process ID, specifying which job to wait for. A process ID is a non-negative decimal integer.

If there is no job matching the operand, the built-in assumes that the job has already finished with exit status 127.

Errors

The following error conditions causes the built-in to return a non-zero exit status without waiting for any job:

  • An operand is not a job ID or decimal process ID.
  • A job ID matches more than one job.

If the shell receives a signal that has a trap action set, the trap action is executed and the built-in returns immediately.

Exit status

If you specify one or more operands, the built-in returns the exit status of the job specified by the last operand. If there is no operand, the exit status is 0 regardless of the awaited jobs.

If the built-in was interrupted by a signal, the exit status indicates the signal.

The exit status is between 1 and 126 (inclusive) for any other error.

Examples

See The wait utility for an example of using the wait built-in without job control.

Using wait with job control and examining the exit status:

$ set -m
$ sleep 10&
$ kill %
$ wait %
$ echo "Exit status $? corresponds to SIG$(kill -l $?)"
Exit status 399 corresponds to SIGTERM

Subshells cannot wait for jobs in the parent shell environment:

$ sleep 10&
$ (wait %)
$ echo $?
127
$ kill $!

In the above example, wait treats the job % as an unknown job and returns exit status 127.

Compatibility

The wait built-in is specified in POSIX.1-2024.

Many existing shells behave differently on various errors. POSIX requires that an unknown process ID be treated as a process that has already exited with exit status 127, but the behavior for other errors should not be considered portable.

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. 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

Variables in arithmetic expressions appear as bare names. Variable names can include Unicode letters, Unicode digits, and ASCII underscores (_), but cannot start with a 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.

Compatibility

POSIX.1-2024 defines arithmetic expressions on the basis of C.

POSIX requires support for signed long integers. This implementation uses signed 64-bit integers, which is at least as wide as long on all common platforms. Future versions may support wider integers. Other implementations may only support narrower integers.

POSIX does not require support for the ++ and -- operators. Dash 0.5.12 treats the ++ prefix operator as two + operators, effectively making it a no-op.

Index

Symbols