Previously known as BetterScripts POSIX Suite.
An ever growing suite of libraries which aim to make portable scripting easier, safer, and more efficient written for scripts written for "command interpretation services and common utility programs" as defined in the "Shell and Utilities" volume of the POSIX.1-2008 standard.
The suite libraries provide commands that enable commonly undertaken tasks to be accomplished more easily, while maintaining maximum portability, by providing more advanced features on top of the POSIX.1 standard, and providing workarounds for environments or utilities which can cause problems.
To the extent possible, every library in the suite is self-contained and has no dependencies outside the library except for a compatible POSIX.1 environment.
While some libraries may provide for being directly executed (i.e. as indistinguishable from a binary command), most libraries are intended to be "sourced" into any script where the library commands will then be available.
- Emulated arrays for any shell.
- Emulated double ended queues, queues, and stacks for any shell.
- The use of these is recommended ahead of other data types (e.g. emulated arrays) wherever possible as they are much faster.
- Both queues and stacks are specializations of double ended queues and so are provided in the same library.
- Argument processing made easy - like
getoptorgetoptsbut much more powerful, while requiring less work.
- Wrapper for
libgetargs.shwhich allows invoking without needing to be sourced.
- Emulated associative arrays for any shell.
- Commands to assist with path processing.
- Includes commands to retrieve path information like size, owner, etc.
- Provides a portable
mktempalternative.
- Wrapper for
libpath.shwhich allows invoking thepath_mktempwithout sourcing the library.
- Commands assist with string processing.
- Includes commands to safely and easily trim, truncate, substitute values, and more.
Several variables affect the libraries, some of these should be set to specific values for the libraries to work as intended1, while others determine library configuration.
TL;DR:
- The expression
${LC_ALL:-${LC_CTYPE:-${LANG:-POSIX}}}SHOULD evaluate toPOSIXorC; other values MAY work, but are NOT generally supported; - The locale of the currently active shell can not be changed - the locale in effect for a shell is always the locale in effect when the shell was invoked;
- Using the correct locale for the data being processed is essential - multi- byte characters can not be processed correctly with many operations unless a suitable locale is currently in effect.
- Some libraries (e.g.
libstring.sh) make significant attempts to be correct using any locale, but this relies on the underlying tools supporting the specific locale - such support varies by platform and is impossible to test for portably.
Details:
All libraries assume the POSIX.1 defined default environment is in effect.
Much of the POSIX.1 standard is only defined for this environment.
- All libraries are locale agnostic to the extent possible, however much of the standard is only defined when the locale is the POSIX locale - implementations can (and do) provide different behavior with other locales.
- In most cases any locale should work, and in cases where there are known issues with implementation behavior outside the POSIX locale, libraries may be able to set the locale directly to avoid issues (though this is not always possible).
- There are several standard defined locale variables, namely:
LANG,LC_COLLATE,LC_CTYPE,LC_MESSAGES,LC_MONETARY,LC_NUMERIC,LC_TIME, andLC_ALL. The variableLANGprovides a default for any unset variables, whileLC_ALLoverrides all other variables. TypicallyLC_CTYPEis the variable that controls if utilities behave according to the standard, while other variables ensure processing is in accordance with locale settings (e.g.LC_MESSAGESmay change the language used for error messages).2 - Although the POSIX locale does not recognize multi-byte characters the standard requires text in the POSIX locale is processed as bytes so multi-byte characters can be processed correctly in many cases, however some operations will behave in unexpected ways.
- Many systems now provide UTF-8 versions of
POSIXandC- note that these are NOT the POSIX locale, although tools may behave as if they are.3 - The locale in use affects which tools are used for some operations.4
TL;DR:
- SHOULD be the standard defined default of
<space><tab><newline>; other values MAY work, but are NOT supported.
Details:
- All libraries are agnostic to the value of
IFSthe extent possible, howeverIFSplays a role in a significant number of fundamental shell operations, and, as such, can cause bugs that may be difficult to understand. - Historically, shell support of
IFShas been somewhat inconsistent, although more recently the situation has improved. Sadly, there remain edge cases where behavior of operations that rely onIFSis different between shells. - No library sets the value of
IFS.
TL;DR:
- SHOULD be the set.
Details:
- In addition to the current locale, GNU tools may also check if this variable is set - if so they will try to match the standard more closely than otherwise would be the case.
- GNU tools are near omnipresent - available for almost any conceivable platform and often installed by default. Given this, and that other implementations may also use this variable, it is prudent to set this.
A number of environment variables affect the functionality of each of the libraries. These include both variables that instruct the library to work-around specific platform issues, and variables that convey user preferences. (See also the information on compatibility.)
Where it is possible, platform specific issues are detected automatically, with the associated variables providing a way to force enabling or disabling specific work-arounds if necessary. Automatic detection should always be preferred - this detects use cases that are actually problematic and not more general issues.
Each configuration variable belongs to a specific CLASS:
- CONSTANT - a configuration option that is read only once when the library
is first sourced and must not be set after this point (the
readonlycommand may be used to enforce this). - VARIABLE - a configuration option that can be modified at any point and may affect the next command.
Additionally, each configuration variable has to a specific TYPE:
- TEXT - has a value that is arbitrary text with constraints defined by each specific variable.
- FLAG - enables or disables specific functionality. The value
0(<zero>) turns a flag OFF, while any other text will turn a flag ON, EXCEPT for flags where automatic detection is applicable where the valueAis special and forces the use of automatic detection. In some cases automatic detection can be delayed until specific functionality is required, in these cases the valueDcan be used to forced delayed automatic detection - if delayed detection is not supported for a flag,DandAare equivalent.5 _A flag that is unset or set but null (i.e. empty) will use an appropriate default value.
Configuration variables are never modified by a library.
Many configuration variables can be set for all libraries with a single, suite wide variable. Where such a suite wide variable is available a library specific variable is always available in addition and has precedence. (Not all libraries use all suite wide settings.)
Suite wide variables are listed in the following sections (with defaults highlighted).
- Type: FLAG
- Class: VARIABLE
- Default: OFF
- [Enable]/Disable library error message output.
- OFF: error messages will be written to
STDERRas:[<IDENTIFIER>]: ERROR: <MESSAGE>. - ON: library error messages will be suppressed.
- Each library also stores the most recent error message in a library specific variable, which is unaffected by this flag.
- Unless otherwise stated, both the library versions of this option and the suite version can be modified between command invocations and should affect the next command.
- Does not affect errors from non-library commands, which may still produce output.
- Type: FLAG
- Class: VARIABLE
- Default: OFF
- Enable/[Disable] causing library errors to terminate the current (sub-)shell.
- OFF: errors stop any further processing, and cause a non-zero exit status, but do not cause an exception.
- ON: any library error will cause an "unset variable" shell exception using
the
${parameter:?[word]}parameter expansion, wherewordis set to an error message that should be displayed by the shell (this message is NOT suppressed byBETTER_SCRIPTS_CONFIG_QUIET_ERRORS). - Unless otherwise stated, both the library versions of this option and the suite version can be modified between command invocations and should affect the next command.
Where possible these are automatically detected - unless there is a problem it is HIGHLY recommended these are not set manually.6
Incorrectly setting these variables will cause errors in many cases (including possible data corruption), while in other cases may result in lost performance.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- [Disable]/Enable using
setoptin Z Shell to ensure POSIX.1 like behavior. - OFF: Use
setoptto set the appropriate options. - ON: Don't use
setopt, even in Z Shell. - Automatically enabled if Z Shell is detected.
- Any use of
setoptis scoped as tightly as possible and should not affect other commands. - If Z Shell is used and the current environment has already been set to be POSIX.1 compliant, forcing this ON may improve performance.
- Z Shell has some defaults that cause non-standard behavior, however also
provides
setoptwhich can be tightly scoped to set options when required without impacting other platforms.7 - WARNING: setting this variable incorrectly will cause errors.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- [Disable]/Enable using only single digit shell parameters, i.e.
$0to$9. - OFF: Use multi-digit shell parameters.
- ON: Use only single-digit shell parameters.
- Multi-digit parameters are faster but may not be supported by all implementations.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- [Disable]/Enable using only
shiftand notshift Nfor multiple parameters. - OFF: Use
shift N. - ON: Use only
shift. - Multi-parameter
shiftis faster but may not be supported by all implementations.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- Disable/[Enable] support for multi-byte character processing within the shell itself (e.g. via wildcards).
- OFF: use fallback code for operations affected.
- ON: use internal shell operations.
- The locale a shell uses is set at invocation and can not be changed for a running shell.
- Default is to run tests for the current shell when a library is sourced to determine if such support is present.
- To support multi-byte characters, requires a shell is invoked using a locale that defines such characters (i.e. not the POSIX locale) and the shell must support them.
- Support for shell string processing of multi-byte characters is far from universal, outside the POSIX locale implementations often continue to use byte processing rather than character processing - in some cases implementations provide partial support, which can be confusing.
- Often lack of support for multi-byte characters is not an issue, however in certain circumstances it can cause problems, including corrupting data.
- Note that some operations are always processed as bytes (not characters) regardless of locale - workarounds for these operations are used based solely on the current locale and are not affected by this flag.
- The costs of running tests are small, but may be measurable for short scripts, setting this variable avoids these costs.
- NOTE: no implementation supports multi-byte characters if invoked in the POSIX locale.
- WARNING: setting this variable incorrectly may lead to errors and data corruption.
- WARNING: fallback implementations may be significantly slower.
- WARNING: set the current locale before sourcing a library will break automatic testing.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- Disable/[Enable] glob/wildcard pattern matching even if the pattern contains known problematic characters.
- OFF: use fallback code for patterns that contain problem characters.
- ON: use shell pattern matching.
- Default is to run tests for the current shell when a library is sourced to determine if the current shell supports these as expected or not.
- Some implementations do not treat all characters in patterns in the way that
is expected - e.g.
\(<backslash>) is an escape character for most implementations, but always literal for others. - The costs of running tests are small, but may be measurable for short scripts, setting this variable avoids these costs.
- WARNING: setting this variable incorrectly may lead to errors and data corruption.
- WARNING: fallback implementations may be significantly slower.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- Disable/[Enable] using character classes of the form
[:alpha:]in "Parameter Expansion". - OFF: use fallback code for operations affected.
- ON: use character classes.
- Using character classes allows some operations to be completed within the shell itself, rather than requiring external commands, but these classes are not always supported for this.
- WARNING: fallback implementations may be significantly slower.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- Disable/[Enable] using character classes of the form
[:alpha:]incasepattern matching expression. - OFF: use fallback code for operations affected.
- ON: use character classes.
- Using character classes allows some operations to be completed within the shell itself, rather than requiring external commands, but these classes are not always supported for this.
- WARNING: fallback implementations may be significantly slower.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- [Disable]/Enable using alternatives to
/dev/nullas a redirection source/target (e.g. for output suppression). - OFF: Use
/dev/null. - ON: Use an alternative to
/dev/null. - Using
/dev/nullas a redirection target is a common idiom, but not always possible (e.g. restricted shells generally forbid this).
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- [Disable]/Enable using the non-standard
egrepinstead ofgrep -E. - OFF: Use
grep -E. - ON: Use
egrep. - While
grep -Eis standard, it is not always supported - implementations that do not support it often provide the non-standardegrepinstead. - WARNING: no check is made that
egrepexists: ifgrep -Eis not availableegrepis assumed to be present.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- [Disable]/Enable using the non-standard
fgrepinstead ofgrep -F. - OFF: Use
grep -F. - ON: Use
fgrep. - While
grep -Fis standard, it is not always supported - implementations that do not support it usually provide the non-standardfgrepinstead. - WARNING: no check is made that
fgrepexists: ifgrep -Fis not availablefgrepis assumed to be present.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic> (delayed)
- Disable/[Enable] support for multi-byte character processing with
tr. - OFF: use fallback code for operations affected.
- ON: use
tr. - Default is to run tests when
tris needed to determine if such support is present. - Some common implementations of
trlack multi-byte character support, regardless of the locale in use. This includes GNUtr. Although such implementations should not corrupt data, they will also not process multi- byte characters correctly. (e.g. the commontr '[:upper:]' '[:lower:]'fails to work as expected). - The costs of running tests are small, but may be measurable for short scripts, setting this variable avoids these costs.
- WARNING: fallback implementations may be significantly slower.
- WARNING: setting this when not required may cause issues if fallback utilities do not support multi-byte character processing themselves.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- Disable/[Enable] using character classes of the form
[:alpha:]withtr. - OFF: use fallback code for operations affected.
- ON: use character classes.
tris one of the fastest commands available for certain tasks, but if it does not support character classes, alternatives may be required.- WARNING: fallback implementations may be significantly slower.
- Type: FLAG
- Class: CONSTANT
- Default: <automatic>
- Enable/[Disable] using
hexdumpin preference tood. - OFF: use
odwhenever possible. - ON: use
hexdumpeven ifodis available. - Has no effect unless both
odandhexdumpare available. - Some systems lack
od, but providehexdumpfor the same purpose, while many provide both. This flag allows the use of eitherodorhexdumpto be preferred. - The default is to use
odif available andhexdumpotherwise. If neither command is available different (situation dependent) methods for generating data are used. - Provided for cases where
hexdumpis more capable thanod
- Type: TEXT
- Class: CONSTANT
- Default:
/dev/urandom - Specify a source for random data.
- MUST be either the special value
awkor a path to use as a source for random data. - If specified as a path, the path MUST be readable, and MUST behave like
/dev/urandom. - If specified as
awk, random data is generated byawk- this is insecure as the data generated is of poor quality and likely to be easy to guess. - Has no effect if neither
odnorhexdumpis available. - NOTE: The commonly present
/dev/randomand/dev/urandomare not standard, while they can be used similarly they do not behave the same way in all circumstances in all implementations. Care should be taken to ensure they work as expected on any platform they will be used. - WARNING: both
/dev/randomand/dev/urandomcan block - when this may occur varies by system, but care must be taken when using these to avoid stalls.
- Type: FLAG
- Class: VARIABLE
- Default: OFF
- Disable/[Enable] using
ARGVwithinawk- enabling gives significantly better performance, but is subject to some limitations. - OFF: Use
ARGVwithinawk. - ON: Avoid
ARGVwithinawk. - When OFF
ARGVwill be used whenever appropriate, if this fails (likely due one of the limitations), the code for the OFF condition will be used to get the required results. This comes with a small cost as theARGVcode must first be run and fail (although this should be relatively fast, it does have an impact). - This is not autodetected as the point is not to test if
ARGVis available (it is assumed to be), but if it should be used for performance reasons. There is no real way to test this it will be system and data specific.
Each library provides a number of variables that are set by the library to convey information outside of command invocation.
These variables must not be set by external commands except if this is
explicitly permitted. Variables may use the readonly command to enforce
this.
Along with any library only information variables, every library also provides a version of some standard variables:
- A whole number >= 1.
- Incremented when there are significant changes, or any changes break compatibility with previous library versions.
- Follows Semantic Versioning v2.0.0.
- A whole number >= 0.
- Incremented for significant changes that do not break compatibility with previous versions.
- Reset to 0 when
BS_<LIBRARY>_VERSION_MAJORchanges. - Follows Semantic Versioning v2.0.0.
- A whole number >= 0.
- Incremented for minor revisions or bugfixes.
- Reset to 0 when
BS_<LIBRARY>_VERSION_MINORchanges. - Follows Semantic Versioning v2.0.0.
- A string indicating a pre-release version.
- Always null for full-release versions.
- Possible values include
alpha,beta,rc, etc, (a numerical suffix may also be appended). - Follows Semantic Versioning v2.0.0.
- Full (numerical) version combining
BS_<LIBRARY>_VERSION_MAJOR,BS_<LIBRARY>_VERSION_MINOR, andBS_<LIBRARY>_VERSION_PATCHas a single value. - Can be used in numerical comparisons.
- Format is
MNNNPPPwhere,Mis theMAJORversion,NNNis theMINORversion (3 digit, zero padded), andPPPis thePATCHversion (3 digit, zero padded).
- Full version combining
BS_<LIBRARY>_VERSION_MAJOR,BS_<LIBRARY>_VERSION_MINOR,BS_<LIBRARY>_VERSION_PATCH, andBS_<LIBRARY>_VERSION_RELEASEas a formatted string. - Format is
BetterScripts '<library>' vMAJOR.MINOR.PATCH[-RELEASE].
- Stores the error message of the most recent library error.
- ONLY valid immediately following a command from the appropriate library for which the exit status is not a success code.
- Valid even when error output is suppressed.
- Set (and non-null) once the library has been sourced.
- Dependant scripts can query if this variable is set to determine if a specific library has been sourced.
- Also serves as a guard to avoid errors caused by sourcing a library multiple times.
Each library supports limited debug information that can be controlled using
environment variables. Library specific variables take precedence. The widely
used DEBUG environment variable provides a default if all other variables
are unset.
In particular, debug output helps determine which code path is used for those commands that vary implementation depending on the capabilities of the current platform. (In some cases these paths are determined dynamically.)
- Type: FLAG
- Class: VARIABLE
- Default: OFF
- Enable/[Disable] library debug message output.
- OFF: no debug messages will be displayed.
- ON: debug messages will be displayed.
BS_<LIBRARY>_DEBUGdefaults to the expression${BS_DEBUG:-${DEBUG:-0}}.- Also enables
BS_<LIBRARY>_CONFIG_DEBUG.
- Type: FLAG
- Class: VARIABLE
- Default: OFF
- Enable/[Disable] library debug message output for configuration only.
- OFF: no debug messages will be displayed.
- ON: debug messages will be displayed.
BS_<LIBRARY>_CONFIG_DEBUGdefaults to the expression${BS_CONFIG_DEBUG:-${BS_<LIBRARY>_DEBUG:-0}}.- Configuration debug is only displayed on sourcing.
- Type: TEXT
- Class: VARIABLE
- Default:
2 - Set a target for debug messages.
- If the target is a single digit or is preceded by an
&(<ampersand>) output is redirected using>&(i.e. it is treated as a file descriptor); if the target is not a single digit or is preceded by a>(<greater-than-sign>) output is redirected using>>(i.e. it is treated as a file). Any&(<ampersand>) or>(<greater-than-sign>) prefix is removed before use. - In some cases
STDERRis suppressed by commands - unless another target is specified this may lead to some debug messages also being suppressed. - If specified as a file descriptor other than
1or2the descriptor MUST be opened before it is used. This can be accomplished in multiple ways, the simplest of which is to use something similar to{ ...; } N>&1whereNis the relevant file descriptor and any code that uses the descriptor is enclosed within the braces. Alternatively theexeccommand can be used to similar effect.
- SHOULD be set to the location of the
shtoolkitfiles. - Multiple paths may be specified - formatted like the standard variable
PATH. - Useful for users if libraries are not installed in a location that is
available in
PATH. - Currently used only by test helper scripts.
As each shtoolkit library is designed to be independent, versioning is on a
per-library basis.
Each release of shtoolkit as a whole is given a version of the form vYY.N
where YY is the year of the release and N is an integer >=1 and is
incremented for each release within a given year.
The shtoolkit release version is for reference only and is not present in code
and can not be quired. Version checks should be made against individual
library versions.
- POSIX.1-2008
- FreeBSD SYSEXITS(3)
- Although not a standard, the values specified by SYSEXITS are widely used and are the only common exit codes generally available.
- Libraries use these values wherever possible, however
other exit codes may occur:
- Values returned by external commands are propagated where possible and appropriate.
- As per POSIX.1 the value
1if used forfalsefor commands that require reporting a non-success, non-error exit status.
- Semantic Versioning v2.0.0
- Each library has its own version number, each of which complies with Semantic Versioning v2.0.0.
- Some libraries may provide version numbers for additional purposes, these also follow Semantic Versioning v2.0.0, but may not include all elements.
- Inclusive Naming Initiative.
The provided Makefile has targets that allow for installation
of both libraries and documentation in configurable locations (by default
libraries are installed in /usr/local/bin, Markdown documentation in
/usr/local/share/doc and man page documentation in the appropriate
/usr/local/share/man directory for the documentation category - note that
these are not POSIX.1 specified).
Most of the libraries are intended to be sourced by other scripts using the
. (aka dot) command, for which the standard says:
If file does not contain a <slash>, the shell shall use the search path specified by PATH to find the directory containing file. Unlike normal command search, however, the file searched for by the dot utility need not be executable.
As such libraries are installed as non-executable unless direct invocation is supported for a specific library.
More information about installation is available by invoking the help target
from the Makefile, i.e. make help.
Up-to-date versions of documentation for each library is always present in
the main repository in both Markdown and
man page formats.
Much of this documentation is generated from other files within the suite, with
Markdown documentation for libraries being generated from comments in the
libraries themselves, while man page documentation is generated from
Markdown documentation for both common and library documentation.
All Markdown documentation aims to be compatible with the original
Markdown specification, with reference to
CommonMark to resolve any ambiguities. Although an extension to
the original standard, footnotes are used throughout the Markdown
documentation as they are highly useful, widely supported, and acceptably
rendered by Markdown flavors that do not support them.
Documentation can be regenerated using the suite provided Makefile.
Library commands document arguments with a tag indicating argument usage:
- in: provides data TO the command.
- out: receives data FROM the command.
- in/out: provides data TO AND receives data FROM the command.
- ref: an additional tag indicating the argument is passed by NAME instead
of VALUE.
- For a typical POSIX.1 variable this means omitting the
$from the name when passing it to the command, i.e. instead of passing$Variable(or${Variable}) useVariable. - Only POSIX.1 compliant names are permitted. Due to the security
considerations of using
evalwith arbitrary text POSIX.1 names are enforced for all variable names; providing a non-standard name will cause an error (even if the name is supported by the current shell). - Variables passed by name are GLOBAL variables. The use of
localvariables (as supported by many shells) will not work as expected. (POSIX.1 has no concept oflocalvariables.)8
- For a typical POSIX.1 variable this means omitting the
Command options, where available, can be specified as POSIX.1 style single
character options following a single - (<hyphen>) character, and as GNU
style long options following a two - (<hyphen>) characters.
Unless otherwise specified:
- POSIX.1 style single character options can be combined into a single multi- character option;
- option arguments can be specified as immediately following the corresponding
option, as a suffix to a single character option, or appended to a multi-
character option using an
=(<equals>) character.
Options MUST precede operands.
The special argument -- (<hyphen><hyphen>) can be used to disable option
processing and treat all subsequent arguments as operands.
All of shtoolkit is designed to be supported in any environment that is
compatible with the "command interpretation services and common utility
programs" as defined in the "Shell and Utilities" volume
of the POSIX.1-2008 standard.
The number of environments that are at least partially POSIX.1 compliant is enormous - even if it were possible to test all of them, access to many is difficult as they are tied to proprietary/specialist systems. Therefore, much of the compatibility work for the Suite is based on resources such as "autoconf: Portable Shell Programming"). While such resources are incredibly useful, they often omit details such as the specific platforms for which problems occur, or even dates for when the problem was discovered or last seen. The result is that it is highly likely some of the workarounds implemented are unnecessary, and other necessary workarounds have been omitted.9
It is likely that any POSIX.1 like environment that supports
"Parameter Expansion", modern style
"Command Substitution" (i.e. $(command)) and
"Arithmetic Expansion" will at least partially support
shtoolkit10.
Additionally:
- The POSIX.1 standard has remained relative consistent between versions (as
relates to functionality required by
shtoolkit), although the POSIX.1-2008 version of the standard is the reference version used for creatingshtoolkitit is likely that earlier versions will also be supported. - Non-compliant shells and utilities may be supported by specific libraries, or specific commands within those libraries.
- A shell and/or utilities which are not supported may still be able to make
use of
shtoolkit- any such tool is termed compatible. The difference between shells and utilities which are supported and those which are compatible is that any erroneous behavior specific to the latter is not technically a bug and unlikely to be addressed. - Commands are designed to be functionally equivalent regardless of the
value of any of the standard specified shell options (e.g.
errexit,nounset, etc). - Where a shell or utility is known to deviate from the functionality required by a library a work-around may be provided if it is relatively simple, performant, and can be scoped to only affect library commands.
- Some common, but non-standard functionality is supported, for example, "restricted" shells.
- Tests for suite libraries are provided along with a test harness in which they are run. These are not primarily intended to determine platform support, but are designed more for regression testing. Additionally, the test harness, while POSIX.1 compliant, it currently require sa more capable platform than that of individual libraries. That said, if tests run successfully for a specific platform it is likely the platform will be fully supported.
All libraries have been tested in multiple operating systems including Ubuntu, Oracle Solaris, FreeBSD, OpenBSD, and Windows Subsystem for Linux.
Multiple implementations of "Shells and Utilities" have also been tested including:
sh11,bash,bosh,busybox,dash,ksh88,ksh93,mksh,modernish,osh,oksh,pdksh,posh,yash, andzsh(including "restricted" versions of these shells where known to exist) - all shells are tested in "default" mode along with any POSIX.1 compatibility mode.Various implementations of utilities have also been tested.
Note that the presence of a shell in the list of those tested does not imply a shell is supported for all libraries and in all use cases.
There are a significant number of implementations of POSIX.1 compliant shells, and each of these has it's own quirks, bugs, and interpretation of the standard.
While most shells are possible to coax into working as expected (even when they do not do so by default), there are some shells which are problematic in one way or another (or have versions known to be). Those included in this section are those that benefit from some remarks - this is in no way a comment on these particular implementations.
Work to better accommodate problematic shells and shells not yet tested remains ongoing.
Legacy shells that do not support things like modern command substitution syntax, arithmetic expansion, and parameter expansion remain unsupported.
busybox is widely available and fully supported, however, not all
distributions of busybox are created equal and some commonly available
versions are significantly less capable than others.
By design busybox is enormously customizable at build time and it is not
uncommon for binary distributions to omit a lot of the optional code.
Unfortunately there appears to be no way to determine what configuration was
used to build a specific version and so it is not easy to use any potential
workarounds that might help, and so some things may fail with some versions of
busybox and not others.
Tests on builds using defconfig work as expected (in line with other shells).
Additionally v1.36 of busybox is known to have numerous issues related
to awk which result in segmentation faults in a number of cases. These issues
are fixed as of v1.37.
For these reasons the test harness has an option --no-busybox which removes
busybox from the list of known shells when running tests - if tests fail
only in busybox these are likely false positives and can often safely be
ignored.
ksh93 version Version AJM 93u+ has issues with the test harness that cause
a number of false negatives. These seem to be related to problems capturing
STDERR via command substitution in certain circumstances. A number of tests
are therefore skipped with this version of this shell as they do not work.
These failures are with the test harness only. They do not affect the usability of this version of this shell with the libraries themselves.
Currently the only known shell not to be automatically used by the test
harness and, currently, can not be tested as it does not correctly process
command line arguments with -c.
A number of "binaries" are provided as alternative methods of accessing specific functionality from the libraries. Each of these is simply a wrapper script that sources the relevant library and invokes a single function within the library with the given arguments.
These scripts provide no additional functionality beyond that available in the libraries themselves.
Importantly, though, these scripts will execute using sh for the current
system - on systems where sh is a unique shell that is less capable than
required, these commands will fail.12
Every library provided relies on many of the standard specified utilities
for functionality beyond that provided by the shell itself - such as sed, and
awk. In some systems the default versions of these utilities are not
sufficiently POSIX.1 compliant - i.e. they do not provide the functionality
required by the libraries. For many such systems, more capable versions of these
utilities are available, only they are not the default tool found via PATH.
While it would be possible to provide configuration points for each such utility used, this quickly becomes unwieldy and can somewhat obfuscate the implementation details.
Instead, workarounds are provided for some of the known issues for utilities - often by way of using a different utility. These are automatically enabled when required, but can also be forced if automatic detection fails. This works well in many cases, but it requires a reasonable workaround be available, which is not always true.
For any system that has a default utility that is not sufficiently capable but that has an alternative version available that is it is relatively trivial to ensure the libraries use the more capable tool without requiring specific configuration options:
- Set
PATH:- the most obvious method to accomplish this is to alter the
PATHvariable to ensure the more capable tool is located before the alternative - this can be changed at system level, or just at script level
- the most obvious method to accomplish this is to alter the
- Use an alias:
- For example:
alias awk=/usr/local/bin/nawk - defining an alias that has the same name as the utility will cause the alias to be invoked instead of the utility
- note than some shells disable alias expansion in non-interactive shells by
default (e.g.
bash)
- For example:
- Use a function:
- For example:
awk() { echo 'Using nawk' >&2; /usr/local/bin/nawk "$@"; } - mostly equivalent to an alias, but aliases are limited in functionality
- For example:
In all cases, provided these configurations are in place when the given utility is invoked they alternative will be used instead.13
TL;DR:
- Pattern matching using wildcards, or Regular Expressions is supported for some commands in libraries;
- Support for specific expressions is implementation dependent;
- In particular, Wildcard patterns will use a fallback implementation in certain circumstances - this is significantly slower, but required to provide portable behavior;
- The locale in effect changes what an expression will match - it is not possible to alter the locale for a currently running shell.
Details:
Some commands support pattern matching in various situations. Such commands support either wildcards, or regular expressions, or more often both.
The type of pattern matching used is specified the same way in all libraries,
via either a grep like option:
-E,--ere, or--extended-regexp, for "Extended Regular Expressions"-G,--bre, or--basic-regexp, for "Basic Regular Expressions"-W,--glob, or--wildcard, for "Pattern Matching Notation"
or a test like primary:
-[not]ere,-[not]matchex,-[not]matchere,=~, or!~for "Extended Regular Expressions"-[not]bre,-[not]matchand-[not]matchbre, for "Basic Regular Expressions"-[not]likefor "Pattern Matching Notation"
In some cases the test like primaries also support flags that affect how the
comparison is performed, these flags are specified as a suffix to the primary,
following a single : (<colon>) character:
s(as in-bre:s) specifies Single Line Mode;m(as in-ere:m) specifies Multi-line Mode.
In Single Line Mode some operations can be made significantly faster, however
it is not possible to match <newline> characters (or match across line
breaks), while Multi-line Mode has no such restrictions. If no suffix is
specified, m is implied.14
Note that using s may decrease performance if values contain
<newline> characters, although this usage is supported.
PORTABILITY
- From testing it seems that in many cases "Basic Regular Expressions" tend to be the most consistently well supported across implementations, in addition they also are often faster than "Extended Regular Expressions" and the fallback implementations for wildcards. As such, for maximum portability with the best performance in all cases it is advisable to use "Basic Regular Expressions" when possible, however, it remains important to test specific expressions on any platform they may be used.
TL;DR:
- Wildcard patterns are implemented using the shell whenever possible - the locale the shell was invoked with determines what can be matched;
- Support for specific expressions is implementation dependent;
- Fallback implementations are provided for shell implementations that do not handle wildcard patterns as expected - this is significantly slower;
- Expressions either match an entire value or do not match (i.e. exactly
like
case).
Details:
Wildcard pattern matching, also known as globbing, is defined in the standard as "Pattern Matching Notation", and is implemented in the shell itself.15
Where library commands provide wildcard support:
- patterns only match if they match an entire value (i.e. the pattern is "anchored" to both the start and end of a value);
- only the characters
*(<asterisk>),?(<question mark>), and[(<left-square-bracket>) are special - all other characters are literal;16 - patterns can match
<newline>characters, either explicitly or with the use of*(<asterisk>) or?(<question mark>).
As a relatively simple form of matching being provided by the shell itself, wildcard matching is often the best performing of the pattern matching options.
PORTABILITY
- Some characters are problematic for some implementations:
- the characters
\(<backslash>),((<left-parenthesis>), and)(<right-parenthesis>) are all known to be handled differently in different implementations, making portable usage difficult; - if a pattern uses one of these characters and the current implementation does not process it as expected, wildcard matching will be handled using the fallback implementation (see below).
- the characters
- Using a
](<right-square-bracket>) inside a bracket expression (i.e.[...]...]) is difficult (or impossible) to do portably. - Escaping characters can be achieved using
[*],[?], or[[]. (This avoids the noted issues with\(<backslash>).) - Some implementations provide additional pattern matching operators using
specific sequences (commonly
*(...),+(...),?(...),@(...),!(...), though others are possible). These can generally be disabled/enabled using implementation specific options settings and are often disabled by default. These extensions may or may not affect the commands in this library17, to avoid any potential issues it is advisable to write any such sequences as[*](...), etc. - The locale in effect for patten matching is always the locale in effect when the shell was first invoked. It is not possible to alter the locale of a currently running shell.
In various situations wildcard matching does not work as might be expected - in these cases fallback code is used that is markedly slower, but more predictable.
Fallback implementation(s) are written using awk and are required:
- for wildcards containing
\(<backslash>),((<left-parenthesis>), or)(<right-parenthesis>) if the current shell does not handle these as expected; - if the current shell does not support multi-byte characters correctly with wildcards and the current locale is not the POSIX locale;
- the shell was invoked in the POSIX locale but the current locale is different.
Tests are performed when a library is sourced to determine if any of these apply. (See BETTER_SCRIPTS_CONFIG_SHELL_SUPPORTS_PORTABLE_GLOB and BETTER_SCRIPTS_CONFIG_SHELL_SUPPORTS_MBC.)
TL;DR:
- Regular Expression patterns are implemented using
awk,sed, orgrepdepending on the use case and configuration; - Support for specific expressions is implementation dependent;
- Different utilities can and do support different expressions;
- In contrast to Wildcard patterns, the locale in use can be modified.
Details:
Both "Basic Regular Expressions" (BRE) and "Extended Regular Expressions" (ERE) are supported.
Patterns are not anchored by default and will match if any part of
the value matches the regular expression (e.g. the expression . will always
match any value containing one or more characters).
In Multi-line Mode, BRE are implemented using sed, while ERE use
awk.18
In Single Line Mode, both BRE and ERE use grep.
Note that as different tools are utilized in each mode it is possible that
support for specific expressions may differ (e.g. on Solaris the default awk
does not support ERE that the default grep supports).
Note that in multi-line mode anchors will match the start (^) and end ($)
of the entire value (i.e. not the start and end of a line).19
PORTABILITY
- Different modes are implemented using different tools, as such behavior may be different between modes (e.g. supported expressions may differ).
- Although literal
<newline>characters are permitted in both BRE and ERE (for supported commands), it is highly advisable to use\n(i.e.<slash>n) instead. - Both BRE and ERE are subject to a great many portability issues,
with some implementations being significantly less capable than the
standard requires. The
autoconfdocs contains possibly the most extensive list of issues: seeawk,grep, andsed. - BRE seem to be more consistently well supported than ERE.
- The complexity of an expression can change how portable it is - of particular note are optional (or null) matches, which can trip up less capable engines. These seldom produce errors, instead only incorrect matches are produced.
TL;DR:
- All libraries try to balance performance, portability, and configurability;
- Certain configuration options can alter performance;
- The largest single factor affecting performance is the implementation(s) of the utilities used.
Details:
- Libraries have been written to maximize performance without sacrificing configurability, safety or utility - with a general philosophy of "you don't pay for what you don't use".
- For most use cases library performance should not be an issue and will likely be far outweighed by other factors.
- Where library performance is an issue, configuration of each library can have a significant affect on performance. Where configuration is known to affect performance, this is noted.
- The most significant factor in the performance of any library is the specific
external commands used by a library:
- The shell used is the single most significant factor, for example,
bashis highly user friendly and provides many advanced tools beyond those required by the standard, however the much less well specifieddashperforms significantly better for all suite libraries. - Utilities like
sed,grep,awk, etc. are available in multiple implementations, each of which has it's own performance characteristics.
- The shell used is the single most significant factor, for example,
- Many libraries provide emulated versions of data structures that are not normally available. These are stored in in a standard shell variable which is manipulated using standard utilities or the shell command language. Performance of these data structures is highly dependent on the size of the data stored. Although implementation dependent, shells tend to be optimized for processing short strings, with strings that may be hundreds or thousands of characters long performance can rapidly decrease.
There are many choices made in the implementation of the tools that may seem unusual, however, largely these choices have been made for good reason.
Of particular note are the extensive use of case where test might be
expected and the use of quotes where they may not be required.
The extensive use of case in place of test is for reasons of performance:
replacing test with case has a measurable impact on the performance of the
tools, sometimes dramatically so.
There are literally hundreds, perhaps thousands, of libraries for shell
programming. A search for any of libsh, shlib, libshell, shelllib,
shboost, or shellboost, for example will return dozens upon dozens of
libraries - most of which are designed for shell programming. However, the
quality, scope, and usefulness of these varies greatly, with common issues
including: targeting a limited number of shells; limited scope; poor
documentation; poor portability.20
While many of these libraries are undoubtedly useful, even the best available
were found to be, in one way or another, unsuitable for the purposes that
shtoolkit now exists to fulfill.
No library code was originated by AI.
AI has been used in the following ways:
- Auditing: all code and documentation (including this file) has been audited by AI for correctness, including but not limited to checking for bugs and security issues, verifying that comments accurately reflect the code they document, and confirming that documentation is written clearly and unambiguously.
- Fixes: where an audit identifies an issue, AI will issue a fix. Every change is fully reviewed before being accepted (and not every fix is accepted); nothing is committed unreviewed.
- Tests: most tests are hand-written; some have been added by AI, typically simple additions extending existing coverage where it was previously missing.
All libraries are written first by hand, to a fully functional level - only once a library is felt to be near release ready is AI involved (in the ways noted above).
Note that while there is significant disagreement over the use of AI in
general, and it is, understandably, anathema for many, its use has permitted
shtoolkit to be maintained and updated to an extent that would otherwise not
have been possible (due to a number of factors).
The tools and libraries in shtoolkit are subject to the limitations imposed
by the particular environment in which they are invoked. Each implementation of
the required utilities and command execution environment will have specific
limitations that may be different to those in another implementation and may
change between versions of the same utilities.21
For most use cases it is deemed unlikely that these limitations will be an issue, however, there will be cases where some limitations may cause problems.
It is impossible to determine all the possible limitations that may exist or may be of issue (even when considering only those specified in the standard). However, of the known limitations, perhaps the most likely to be encountered across multiple libraries is the command line length limit, which can be encountered in a number of scenarios, and in unexpected ways.
The standard specifies this as {ARG_MAX} and defines it as:
The number of bytes available for [a] new process' combined argument and environment lists... It is implementation-defined whether null terminators, pointers, and/or any alignment bytes are included in this total.
The value for any particular environment can be queried using the command
getconf ARG_MAX, though this value can only be used as a guide since it is
impossible to know how many bytes any command will require in advance - indeed
the command line length limit does not apply to all commands: this limit does
not apply to any shell builtin commands.22
For a modern system the value of the command line length limit can be several million bytes, while older systems it can be significantly less, though it is possible to increase the available command line length for commands, by, for example:
- reducing the number (and size) of exported variables;
- avoiding characters that use more than a single byte;
- changing system settings to increase the size of
ARG_MAX.23
Importantly, any variable which is exported and also has it's contents used as an argument to a command will count TWICE towards this limit.
It is recommended that variables containing library data are not exported.
While the Linux kernel uses ARG_MAX as defined in the standard, it adds an
additional restriction which is not standard - each argument passed to a
command is subject to a maximum length as defined by MAX_ARG_STRLEN. Sadly,
this value can not be directly queried in the shell, but appears to be hardcoded
in the kernel as PAGE_SIZE * 32 which on many (all?) systems appears to equate
to 131072.
Any single argument larger than this can never be passed to a command in Linux.
All libraries require a number of internal commands and variables to provide
the provided functionality, these are distinguishable from other values by a
prefix: commands these are prefixed with fn_bs_; while variables are prefixed
with g_BS_, c_BS_, or i_BS.
These are strictly for internal usage and must not be invoked or referenced outside the library to which they belong.
Footnotes
-
While it would be possible to set some environment variables to the required values when needed by a specific library (e.g. setting the
POSIXlocale), this is not always easy to do while avoiding changing the state for the invoker and maintaining performance. Setting all variables as part of a command might be possible in many cases, but would require huge lines of code for each command, and setting variables may not even be possible (e.g. standard variables may bereadonlyin a restricted shell, while utilities likeenvcan not be used for shell builtins). Finally, setting these variables to the expected value assumes that other values do not work, which may not be true and may make some uses of the libraries impossible without any real need. ↩ -
Some shells and utilities (most notably those provided by GNU) add additional locale variables to those specified by the standard. These are mostly similar to
LC_MESSAGES, and do not affect general behavior of utilities, however the variableLANGUAGEmay cause issues in some circumstances. ↩ -
There are numerous subtle differences between the UTF-8 versions and the other versions, for example, character classes will contain many more characters in the UTF-8 version. Standard conforming tools need not provide standard conforming behavior for these locales. ↩
-
It is not possible to have literal multi-byte characters in a script file as not all shells can process these (leading to errors on sourcing). The only real way to get a multi- byte character is to use
awkwhich supports octal escapes, however, code points are not unique in locales, leading to problems as it is not possible to determine what character a specific octal escape sequence represents. For a significant time now, UTF-8 seems to be the defacto standard for character encodings, with UTF-8 locales available for most modern systems. Given all this, any tests for multi-byte support assume UTF-8 code points. In the case where the locale is NOT UTF-8 this will likely fail and manual configuration will be required. Note that failure to chose the correct tool will often only result in less optimal performance and is, in most cases, unlikely to result in errors - the main exception to this is for systems that have inconsistent support for multi-byte characters (particularly forawk). ↩ -
Delayed detection is helpful in some cases, particularly when locale is important - without delayed detection the best code path may not be chosen (as the locale may not be set appropriately). Note that delayed detection only ever allows a better code path to be chosen - it should not cause errors if skipped. ↩
-
In some cases it is not possible to detect issues with 100% accuracy, while some tests may cause measurable delays for short scripts - in these cases setting the appropriate variable correctly is recommended. ↩
-
Technically since the default configuration of Z Shell is non-standard it is not supported, however this work-around is provided since it can be easily scoped, and does not notably affect performance, and causes no issues with other environments. Similar work-arounds for other environments are not always possible (e.g. the GNU specific
POSIXLY_CORRECTenvironment variable can not so easily be dealt with). ↩ -
The
localkeyword is widely supported but as it is non- standard how it behaves varies between implementations. Many implementations oflocalmay work for these cases, however some implementations will not. ↩ -
Legacy systems and software can often be found in older organizations, especially where the organizations (and hence the associated systems) are somewhat specialized. While many of these systems are no longer actively maintained by the original manufactures, they continue to be used. Even those systems that are still maintained may contain long obsolete software. For example, Oracle Solaris 11.4 shipped in September 2023, yet contains a version of
ksh88(i.e. the 1988 version of KornShell) - although this shell is largely similar to more modern shells it does deviate somewhat, here it simply serves as an example of how even maintained systems can continue to support very old software. Where practicable, the Suite is intended to support all such systems. (pdkshis also worth mentioning - it continues to be supported on some systems despite not having being meaningfully updated since last century.) ↩ -
Legacy "Command Substitution" (i.e.
`command`) and arithmetic viaexprmay be possible to support if there is significant interest in this, however, "Parameter Expansion" is likely to always be required as there are no real alternatives that do not also have significant drawbacks. ↩ -
While
shis often simply a link to another shell, this is not always true - for some platforms it is a unique shell. ↩ -
Working around this issue is trivial: simply edit the scripts to target a different shell; ensure
/usr/bin/env shinvokes a suitably capable shell; or only use the library versions of commands. In the future install time detection and correction of these issues may be added. ↩ -
These configurations options do not have any affect when a library is parsed, only when the code is actually invoked. However, libraries may invoke utilities during sourcing in order to determine which workarounds to use. It is advisable to ensure these are configured prior to sourcing to avoid any unexpected issues. ↩
-
The difference is effectively the difference between
grepandawkorsed- the latter can match patterns across multiple lines, while the former can not. ↩ -
It is unfortunate that the standard uses the term Pattern Matching to refer specifically to this form of pattern matching. Except where referring directly to the standard, variations on the terms wildcard or glob are preferred across the suite, with the term pattern matching used as an umbrella term that also includes regular expressions. (Historically, "glob" was used only for file matching, however, it is more widely applied now.) ↩
-
Note that
](<right-square-bracket>) is only special if preceded by a[(<left-square-bracket>), while|(<vertical-line>) is only special when used withcase(and is not special for library commands). ↩ -
Although pattern matching is used in several different places in the shell (e.g.
casematches, file expansion, etc.) - each has very slightly different rules (e.g.casesupports|while file expansion does not). Whether or not any extensions will affect specific pattern matching is implementation defined, and not always clearly indicated. ↩ -
Although
expris faster thansedfor BRE matching, it is hard to use portably and has many limitations that do not affectsed- as such it is not used for pattern matching as it can not be guaranteed to function as expected. ↩ -
Although this seems widely supported for the utilities described by the standard, more generally this is an area where regular expression implementations often diverge, which often leads to subtle and difficult to diagnose bugs. Use with care. ↩
-
It would be impossible to list even a fraction of those that are available, however the "Awesome Shell" list provides a curated list of some of the better libraries. ↩
-
Many of these limitations are specified in the standard, with specific constraints, though generally the actual value is "implementation defined". ↩
-
Given that each shell implementation has a different set of commands that are builtin this means that when and where this limit may be hit is very difficult to determine. Additionally some commands are available as both builtin and standalone commands - it is possible to invoke either but only the standalone command is subject to the command line length limitation. For a shell like
busyboxeverything can be a builtin command and the command line length limits may not apply. ↩ -
On at least some systems, the size of
ARG_MAXis tied to other system settings and can be increased by modifying those configurables. This must be done with care, however, as these values affect how many different systems interact and may cause issues that are not immediately obvious. ↩