问题
I mean I want to use unset that is not a shell function itself. If I could do that, I could make sure that command is pure by running
#!/bin/sh
{ \unset -f unalias command [; \unalias unset command [ } 2>/dev/null;
# make zsh find *builtins* with `command` too:
[ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on
If I am using Debian Almquist shell (dash), I think I can rely that \unset is pure. At least I could not define a shell function named unset in dash. Whereas in bash or in zsh I could define unset() { echo fake unset; }, and thereafter I am unable to unset the function: \unset -f unset outputs "fake unset".
Relating to this, in a bash script, one can export a function by export -f <function name> so that it can be used in bash scripts called by the script. However, the same does not work in dash scripts. I wonder, if I have to worry about a command being defined as a shell function outside a script file I am writing, if I am using dash? How about other POSIX compatible shells?
回答1:
Note: The following applies to all major POSIX-compatible shells, except where noted otherwise: bash, dash, ksh, and zsh. (dash, the Debian Almquist Shell, is the default shell (sh) on Debian-based Linux distros such as Ubuntu).
unsethaving its original meaning - a builtin that can undefine shell functions with its -f option - is the key to ensuring that any other shell keyword, command, or builtin has its original meaning.- Starting with an unmodified
unset, you can ensure an unmodifiedshoptand/orcommand, and together they can be used to bypass or undefine any aliases or shell functions that may shadow shell keywords, builtins, and external utilities. - As an alternative to undefining functions,
commandcan be used to bypass them, including those that may have been defined outside of your code, through the environment;
exporting functions, as onlybashsupports, is only one of these mechanisms; different shells have different ones and may support several - see below.
- Starting with an unmodified
Only
dash,ksh, andbashwhen in POSIX compatibility mode guarantee thatunsetwasn't redefined:dashandkshare safe, because they don't allow defining a function namedunset, as you've discovered, and any alias form can be bypassed by invoking as\unset.bash, when in POSIX compatibility mode, allows you to define a function namedunset, but ignores it when you invokeunset, and always executes the builtin, as you've later discovered.- Given that POSIX compatibility mode restricts Bash's feature set and modifies its behavior, it is usually not desirable to run your Bash code in it. At the bottom of this post is an implementation of the workaround you suggest, which temporarily actives POSIX compatibility mode in order to ensure that no
unsetfunction is defined.
- Given that POSIX compatibility mode restricts Bash's feature set and modifies its behavior, it is usually not desirable to run your Bash code in it. At the bottom of this post is an implementation of the workaround you suggest, which temporarily actives POSIX compatibility mode in order to ensure that no
Sadly, as far as I know, in
zsh- and also inbash's default mode - there is no way to guarantee thatunsetitself hasn't been redefined, and there may be other POSIX-like shells that behave similarly.- Calling it as
\unset(quoting any part of the name) would bypass an alias redefinition, but not a function redefinition - and to undo that you would need the originalunsetitself: catch 22.
- Calling it as
Thus, with no control over the execution environment, you cannot write shell scripts that are fully immune to tampering, unless you know that your code will be executed by
dash,ksh, orbash(with the workaround in place).If you're willing to assume that
unsethas not been tampered with, the most robust approach is to:Use
\unset -fto ensure thatunaliasandcommandare unmodified (not shadowed by a shell function:\unset -f unalias command)- Functions, unlike aliases, must be undefined explicitly, by name, but not all shells provide a mechanism to enumerate all defined functions, unfortunately (
typeset -fworks inbash,ksh, andzsh, but dash appears to have no mechanism at all), so that undefining all functions is not always possible.
- Functions, unlike aliases, must be undefined explicitly, by name, but not all shells provide a mechanism to enumerate all defined functions, unfortunately (
Use
\unalias -ato remove all aliases.Then invoke everything with
command [-p], except for functions you have defined. When invoking external utilities, use explicit paths when possible, and/or, in the case of standard utilities, usecommand -p, which uses a minimal$PATHdefinition restricted to standard locations (runcommand -p getconf PATHto see that definition).
Additional info:
Per POSIX, quoting any part of a command name (e.g.,
\unset) bypasses any alias form or keyword form (reserved word in POSIX andzshparlance) by that name - but not shell functions.Per POSIX,
unalias -aundefines all aliases. There is no equivalent, POSIX-compliant command for undefining all functions.- Caveat: older
zshversions do not support-a; as of at leastv5.0.8, however, they do.
- Caveat: older
Builtin
commandcan be used to bypass keywords, aliases, functions inbash,dash, andksh- in other words:commandonly executes builtins and external utilities. By contrast,zshby default also bypasses builtins; to makezshexecute builtins too, useoptions[POSIX_BUILTINS]=on.The following can be used to execute external utilities named
<name>only, across all shells:"$(command which <name>)" ...
Note that whilewhichis not a POSIX utility, it is widely available on modern Unix-like platforms.Precedence of command forms:
bash,zsh: alias > shell keyword > shell function > builtin > external utilityksh,dash: shell keyword > alias > shell function > builtin > external utility- I.e.: In
bashandzshan alias can override a shell keyword, while inkshanddashit cannot.
bash,ksh, andzsh- but notdash- all allow a nonstandard function signature,function <name> { ..., as an alternative to the POSIX-compliant<name>() { ...form.- The
functionsyntax is the prerequisite for:- ensuring that
<name>isn't itself subject to alias expansion before the function is defined. - being able to pick a
<name>that is also a shell keyword;
note that such a function can only be invoked in quoted form; e.g.,\while. - (In the case of
ksh, usingfunctionsyntax additionally implies thattypesetstatements create local variables.)
- ensuring that
dash,ksh, andbashwhen in POSIX mode additionally prevent naming functions for special builtins (e.g.,unset,break,set,shift); the list of POSIX-defined special builtins can be found here; bothdashandkshadd a few more that cannot be redefined (e.g.,localindash;typesetandunaliasinksh), but both shells have additional, non-special builtins that can be redefined (e.g.,type).
Note that in the case ofkshthe above rules apply irrespective of whetherfunctionsyntax is used or not.
- The
Potential sources of environment shell functions in scope for your code:
Note: The simplest way to guard against these is to use the (unmodified)
commandbuiltin (inzshwithoptions[POSIX_BUILTINS]=on, to prevent bypassing of builtins as well) whenever you want to call a builtin or external utility.POSIX mandates that a script specified by its absolute path in environment variable
ENVbe sourced for interactive shells (with some restrictions - see the spec);kshanddashalways honor that, whereasbashonly does so when invoked asshor, in v4.2+, with--posix; by contrast,zshnever honors this variable.- Note: Your code run as a script would normally run in a non-interactive shell, but that's not guaranteed; e.g., your code could be sourced from an interactive script, or someone could invoke your script with, e.g.,
sh -ito force an interactive instance.
- Note: Your code run as a script would normally run in a non-interactive shell, but that's not guaranteed; e.g., your code could be sourced from an interactive script, or someone could invoke your script with, e.g.,
bashhas 2 mechanisms:- Exporting individual functions with
export -fordeclare -fx(the other shells only support exporting variables) - Specifying the full path of a script to source whenever a non-interactive shell is started in optional environment variable
BASH_ENV.
- Exporting individual functions with
kshsupports auto-loading of functions via the optionalFPATHenvironment variable: files containing function definitions located in any directory specified inFPATHare implicitly and automatically loaded.- (
zshsupportsFPATHtoo, but auto-loading functions requires an explicitautoload <name>statement, so unless you specifically ask for a function by a given name to auto-load, no functions will be added to your shell.)
- (
zshsupports sourcing scripts for anyzshinstance (whether interactive or not) via its/etc/zshenvand~/.zhsenvinitialization files.(
dashappears not to support any mechanism for defining functions via the environment.)
Workaround for bash: Ensure that unset has its original meaning:
This workaround is only safe if you know that bash will be executing your script, which, unfortunately, cannot itself be guaranteed.
Also, because it modifies the shell environment (removal of aliases and functions), it is not suitable for scripts that are designed to be sourced.
As stated, it's usually not desirable to run your code in Bash's POSIX compatibility mode, but you can temporarily activate it in order to ensure that unset is not shadowed by a function:
#!/bin/bash
# *Temporarily* force Bash into POSIX compatibility mode, where `unset` cannot
# be shadowed, which allows us to undefine any `unset` *function* as well
# as other functions that may shadow crucial commands.
# Note: Fortunately, POSIXLY_CORRECT= works even without `export`, because
# use of `export` is not safe at this point.
# By contrast, a simple assignment cannot be tampered with.
POSIXLY_CORRECT=
# If defined, unset unset() and other functions that may shadow crucial commands.
# Note the \ prefix to ensure that aliases are bypassed.
\unset -f unset unalias read declare
# Remove all aliases.
# (Note that while alias expansion is off by default in scripts, it may
# have been turned on explicitly in a tampered-with environment.)
\unalias -a # Note: After this, \ to bypass aliases is no longer needed.
# Now it is safe to turn POSIX mode back off, so as to reenable all Bash
# features.
unset POSIXLY_CORRECT
# Now UNDEFINE ALL REMAINING FUNCTIONS:
# Note that we do this AFTER switching back from POSIX mode, because
# Bash in its default mode allows defining functions with nonstandard names
# such as `[` or `z?`, and such functions can also only be *unset* while
# in default mode.
# Also note that we needn't worry about keywords `while`, `do` and `done`
# being shadowed by functions, because the only way to invoke such functions
# (which you can only define with the nonstandard `function` keyword) would
# be with `\` (e.g., `\while`).
while read _ _ n; do unset -f "$n"; done < <(declare -F)
# IN THE REST OF THE SCRIPT:
# - It is now safe to call *builtins* as-is.
# - *External utilities* should be invoked:
# - by full path, if feasible
# - and/or, in the case of *standard utilities*, with
# command -p, which uses a minimal $PATH definition that only
# comprises the locations of standard utilities.
# - alternatively, as @jarno suggests, you can redefine your $PATH
# to contain standard locations only, after which you can invoke
# standard utilities by name only, as usual:
# PATH=$(command -p getconf PATH)
# Example command:
# Verify that `unset` now refers to the *builtin*:
type unset
Test command:
Assume that the above code was saved to file script in the current dir.
The following command simulates a tampered-with environment where unset is shadowed by both an alias and a function, and file script is sourced, causing it to see the function and, when sourced interactively, to expand the alias too:
$ (unset() { echo hi; }; alias unset='echo here'; . ./script)
unset is a shell builtin
type unset outputting unset is a shell builtin is proof that both the function and the alias shadowing the builtin unset were deactivated.
回答2:
funnily enough, you already said the builtin name -- command
$ var="FOO"
$ unset() { echo nope; }
$ echo "${var}"
FOO
$ unset var
nope
$ echo "${var}"
FOO
$ command unset var
$ echo "${var}"
<nothing!>
this doesn't help if you're in a hostile environment where someone has created a command() { :; } function. but if you're in a hostile environment, you've already lost ;).
when it comes to exporting functions into the environment, that's a bash-specific extension and you should not really rely on that. POSIX shells (like dash) do not support that by design.
回答3:
This is what I know what can be done...
#!/bin/bash --posix
# if e.g. BASH_FUNC_unset() env variable is set, script execution cannot
# get this far (provided that it is run as is, not as `bash script ...`)
unset -f builtin command declare ...
saved_IFS=$IFS; readonly saved_IFS
# remove all functions (shell builtin declare executed in subshell)
IFS=$'\n'; for f in `declare -Fx`; do unset -f ${f##* }; done; IFS=$saved_IFS
来源:https://stackoverflow.com/questions/35916983/how-can-you-use-pure-unset-shell-builtin-can-you-write-shell-scripts-that-are-i