bash-hackers-wiki/docs/scripting/nonportable.md

203 lines
8.5 KiB
Markdown
Raw Normal View History

2024-04-02 21:19:20 +02:00
---
tags:
- bash
- shell
- scripting
- portability
- POSIX
- portable
---
2023-07-05 11:31:29 +02:00
2024-04-02 21:19:20 +02:00
# Portability talk
2023-07-05 11:31:29 +02:00
The script programming language of BASH is based on the Bourne Shell
syntax, with some extensions and derivations.
If scripts need to be portable, some of the BASH-specific syntax
elements should be avoided. Others should be avoided for all scripts,
e.g. if there is a corresponding POSIX(r)-compatible syntax (see
[obsolete](../scripting/obsolete.md)).
2023-07-05 11:31:29 +02:00
Some syntax elements have a BASH-specific, and a portable[^1]) pendant.
In these cases the portable syntax should be preferred.
|construct|portable equivalent|Description|Portability|
|---------|-------------------|-----------|-----------|
|`source FILE`|`. FILE`|include a script file|Bourne shell (bash, ksh, POSIX(r), zsh, …)|
|`declare` keyword|`typeset` keyword|define local variables (or variables with special attributes)|ksh, zsh, …, **not POSIX!**|
|`command <<< WORD`|`command <<MARKER`\n`WORD`\n`MARKER`|a here-string, a special form of the here-document, avoid it in portable scripts!|POSIX(r)|
|`export VAR=VALUE`|`VAR=VALUE`\n`export VAR`|Though POSIX(r) allows it, some shells don't want the assignment and the exporting in one command|POSIX(r), zsh, ksh, …|
|`(( MATH ))`|`: $(( MATH ))`|POSIX(r) does't define an arithmetic compund command, many shells don't know it. Using the pseudo-command `:` and the arithmetic expansion `$(( ))` is a kind of workaround here. **Attention:** Not all shell support assignment like `$(( a = 1 + 1 ))`! Also see below for a probably more portable solution.|all POSIX(r) compatible shells|
|`[[ EXPRESSION ]]`|`[ EXPRESSION ]`\nor\n`test EXPRESSION`|The Bashish test keyword is reserved by POSIX(r), but not defined. Use the old fashioned way with the `test` command. See [[web/20230315170826/https://wiki.bash-hackers.org/commands/classictest]]|POSIX(r) and others|
|`COMMAND < <( …INPUTCOMMANDS… )`|`INPUTCOMMANDS > TEMPFILE`\n`COMMAND < TEMPFILE`|Process substitution (here used with redirection); use the old fashioned way (tempfiles)|POSIX(r) and others|
|`((echo X);(echo Y))`|`( (echo X); (echo Y) )`|Nested subshells (separate the inner `()` from the outer `()` by spaces, to not confuse the shell regarding arithmetic control operators)|POSIX(r) and others|
2023-07-05 11:31:29 +02:00
## Portability rationale
Here is some assorted portability information. Take it as a small guide
to make your scripts a bit more portable. It's not complete (it never
2024-03-30 20:09:26 +01:00
will be!) and it's not very detailed (e.g. you won't find information
about how which shell technically forks off which subshell). It's just
2023-07-05 11:31:29 +02:00
an assorted small set of portability guidelines. *-Thebonsai*
!!! warning "FIXME"
UNIX shell gurus out there, please be patient with a newbie like me
and give comments and hints instead of flames.
2023-07-05 11:31:29 +02:00
### Environment (exported) variables
When a new value is assigned to an **existing environment variable**,
there are two possibilities:
The *new value* is seen by subsequent programs
- without any special action (e.g. Bash)
- only after an explicit export with `export VARIABLE` (e.g. Sun's
2023-07-05 11:31:29 +02:00
`/bin/sh`)
2024-03-30 20:09:26 +01:00
Since an extra `export` doesn't hurt, the safest and most portable way
2023-07-05 11:31:29 +02:00
is to always (re-)export a changed variable if you want it to be seen by
subsequent processes.
### Arithmetics
Bash has a special compound command to do arithmetic without expansion.
However, POSIX has no such command. In the table at the top, there's
2023-07-05 11:31:29 +02:00
the `: $((MATH))` construct mentioned as possible alternative. Regarding
the exit code, a 100% equivalent construct would be:
# Bash (or others) compound command
if ((MATH)); then
...
# portable equivalent command
if [ "$((MATH))" -ne 0 ]; then
...
Quotes around the arithmetic expansion `$((MATH))` should not be
necessary as per POSIX, but Bash and AT&T-KSH perform word-splitting on
aritrhmetic expansions, so the most portable is *with quotes*.
### echo command
The overall problem with `echo` is, that there are 2 (maybe more)
mainstream flavours around. The only case where you may safely use an
2024-03-30 20:09:26 +01:00
`echo` on all systems is: Echoing non-variable arguments that don't
start with a `-` (dash) and don't contain a `\` (backslash).
2023-07-05 11:31:29 +02:00
Why? (list of known behaviours)
- may or may not automatically interpret backslash escpape codes in
the strings
- may or may not automatically interpret switches (like `-n`)
- may or may not ignore "end of options" tag (`--`)
2023-07-05 11:31:29 +02:00
- `echo -n` and `echo -e` are neither portable nor standard (**even
within the same shell**, depending on the version or environment
variables or the build options, especially KSH93 and Bash)
For these, and possibly other, reasons, POSIX (SUS) standardized the
existance of [the `printf` command](../commands/builtin/printf.md).
2023-07-05 11:31:29 +02:00
### Parameter expansions
- `${var:x:x}` is KSH93/Bash specific
- `${var/../..}` and `${var//../..}` are KSH93/Bash specific
- `var=$*` and `var=$@` are not handled the same in all shells if the
first char of IFS is not " " (space). `var="$*"` should work
2023-07-05 11:31:29 +02:00
(except the Bourne shell always joins the expansions with space)
### Special variables
#### PWD
[PWD](../syntax/shellvars.md#PWD) is POSIX but not Bourne. Most shells are
2024-03-30 20:09:26 +01:00
*not POSIX* in that they don't ignore the value of the `PWD`
2023-07-05 11:31:29 +02:00
environment variable. Workaround to fix the value of `PWD` at the start
of your script:
pwd -P > dev/null
#### RANDOM
[RANDOM](../syntax/shellvars.md#RANDOM) is Bash/KSH/ZSH specific variable
that will give you a random number up to 32767 (2^15-1). Among many
2023-07-05 11:31:29 +02:00
other available external options, you can use awk to generate a random
number. There are multiple implementations of awk and which version your
system uses will depend. Most modern systems will call 'gawk' (i.e.
GNU awk) or 'nawk'. 'oawk' (i.e. Original/Old awk) does not have the
2023-07-05 11:31:29 +02:00
rand() or srand() functions, so is best avoided.
# 'gawk' can produce random numbers using srand(). In this example, 10 integers between 1 and 500:
randpm=$(gawk -v min=1 -v max=500 -v nNum=10 'BEGIN { srand(systime() + PROCINFO["pid"]); for (i = 0; i < nNum; ++i) {print int(min + rand() * (max - min)} }')
# 'nawk' and 'mawk' does the same, but needs a seed to be provided for its rand() function. In this example we use $(date)
randpm=$(mawk -v min=1 -v max=500 -v nNum=10 -v seed="$(date +%Y%M%d%H%M%S)" 'BEGIN { srand(seed); for (i = 0; i < nNum; ++i) {print int(min + rand() * (max - min)} }')
*Yes, I'm not an `awk` expert, so please correct it, rather than
2023-07-05 11:31:29 +02:00
complaining about possible stupid code!*
# Well, seeing how this //is// BASH-hackers.org I kinda missed the bash way of doing the above ;-)
2023-07-05 11:31:29 +02:00
# print a number between 0 and 500 :-)
printf $(( 500 * RANDOM / 32767 ))
# Or print 30 random numbers between 0 and 10 ;)
X=0; while (( X++ < 30 )); do echo $(( 10 * RANDOM / 32767 )); done
#### SECONDS
[SECONDS](../syntax/shellvars.md#SECONDS) is KSH/ZSH/Bash specific. Avoid it.
2023-07-05 11:31:29 +02:00
Find another method.
### Check for a command in PATH
The [PATH](../syntax/shellvars.md#PATH) variable is a colon-delimited list of
directory names, so it's basically possible to run a loop and check
every `PATH` component for the command you're looking for and for
2023-07-05 11:31:29 +02:00
executability.
2024-03-30 20:09:26 +01:00
However, this method doesn't look nice. There are other ways of doing
2023-07-05 11:31:29 +02:00
this, using commands that are *not directly* related to this task.
#### hash
The `hash` command is used to make the shell store the full pathname of
a command in a lookup-table (to avoid re-scanning the `PATH` on every
command execution attempt). Since it has to do a `PATH` search, it can
be used for this check.
For example, to check if the command `ls` is available in a location
accessible by `PATH`:
if hash ls >/dev/null 2>&1; then
echo "ls is available"
fi
Somewhat of a mass-check:
for name in ls grep sed awk; do
if ! hash "$name" >/dev/null 2>&1; then
echo "FAIL: Missing command '$name'"
exit 1
fi
done
2024-03-30 20:09:26 +01:00
Here (bash 3), `hash` also respects builtin commands. I don't know if
2023-07-05 11:31:29 +02:00
this works everywhere, but it seems logical.
#### command
The `command` command is used to explicitly call an external command,
rather than a builtin with the same name. For exactly this reason, it
has to do a `PATH` search, and can be used for this check.
For example, to check if the command `sed` is available in a location
accessible by `PATH`:
if command -v sed >/dev/null 2>&1; then
echo "sed is available"
fi
[^1]: "portable" doesn't necessarily mean it's POSIX, it can also
mean it's "widely used and accepted", and thus maybe more
2023-07-05 11:31:29 +02:00
portable than POSIX(r