5 ms·
Well, for one thing, it doesn't quote arguments correctly. If you do this... touch "a b" ...it will create one file called "a b". But if you do this...
by adrianmonk 3y ago
Well, for one thing, it doesn't quote arguments correctly.
If you do this...
touch "a b"
...it will create one file called "a b".
But if you do this...
defer touch "a b"
...it will create two files called "a" and "b".
- remram 3y agoYou mean it doesn't quote arguments automatically, it is safe so long as you always quote the command defer 'touch "a b"' (a usability pitfall I know, but probably not fixable easily) edit: maybe escaping could be added though, using sed? DEFER="; ${DEFER}" for arg in "$@"; do DEFER="$(printf -- %s "${arg}" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/") ${DEFER}" done
- kazinator 3y ago#!/bin/sh DEFER= defer() { local i local cmdline="" for arg in ${@+"$@"} ; do [ -n "$cmdline" ] && cmdline="$cmdline " case $arg in *"'"* ) case $arg in *[\"\$\\]* ) cmdline="$cmdline'$(printf "%s" "$arg" | sed -e "s/'/'\\\\''/g")'" ;; * ) cmdline="$cmdline\"$arg\"" ;; esac ;; *'"'* | *['$*?[(){};&|<>#']* | '~'* ) cmdline="$cmdline'$arg'" ;; *' '* | *' '* ) # literal tab between second pair of quotes cmdline="$cmdline\"$arg\"" ;; * ) cmdline="$cmdline$arg" ;; esac done DEFER="$cmdline; $DEFER" # printf "DEFER=%s\n" "$DEFER" # debugging trap "command $DEFER" EXIT } (The quoting code is complicated because it's taken from a program that quotes "neatly". Words that don't need quoting aren't quoted, and single or double quotes are used, whichever is nicer. It's not valuable if all you do is execute, but it could help the debug printf be more understandable.)
- patrec 3y agoNo need for the gobbledygook. Just use printf %q, which will quote neatly, too. defer() { new_cmd="$(printf "%q " "$@")" DEFER="${new_cmd% }; $DEFER" trap "command $DEFER" EXIT } Yeah, %q is not in POSIX, so this won't work with e.g. dash – but trying to stick to POSIX is almost always a dumb idea.
- patrec 3y ago(And, BTW, the original code, for all its escaping gymnastics, will only work correctly in bash anyway)
- kazinator 3y agoDid you spot anything in the for loop that is Bash specific? That was taken from code that doesn't use local and which absolutely has to work in various other shells. I just happened to plant it into a function that uses the local feature. That is not just a Bash feature; it's in the Korn Shell, and Dash has it too.
- patrec 3y agoThe trap EXIT only does the right thing in bash (run on any exit condition, try-finally-style). For dash and unfortunately even for zsh, you'd have to specify multiple signal handlers: > bash -c 'trap "echo cleaning up" EXIT; while :; do sleep 1; done' ^Ccleaning up > dash -c 'trap "echo cleaning up" EXIT; while :; do sleep 1; done' ^C > zsh -c 'trap "echo cleaning up" EXIT; while :; do sleep 1; done' ^C What makes it even more irritating is that you need to manually clear the handlers and kill yourself if you trap INT etc, so you can't just add a few more signals to the trap clause above either. IMO the lack of [[, print %q and sane trap semantics, plus the fact that >99.9% of systems that have a posix shell also have bash means that outside of very specific circumstances (e.g. it needs to run in busybox) trying to limit oneself to posix sh is ill-advised.
- 3y ago
- adrianmonk 3y agoHere's my solution to the problem I pointed out. The basic idea is let the shell do expansion / word splitting on the to-be-deferred command when defer() is called, then save the words into an array, and avoid doing expansion. This should avoid running into quoting problems in the first place. #! /bin/bash defer_run() { while [ ${#DEFER_OFFSETS[@]} -gt 0 ] do # Get / remove last element of offset and length arrays. offset=${DEFER_OFFSETS[-1]}; unset DEFER_OFFSETS[-1] len=${DEFER_LENGTHS[-1]}; unset DEFER_LENGTHS[-1] # Get a slice of the main array corresponding to one deferred # command's word list. Execute that word list as a command. "${DEFER_LIST[@]:$offset:$len}" done } defer() { declare -g -a DEFER_LIST DEFER_OFFSETS DEFER_LENGTHS trap defer_run EXIT DEFER_OFFSETS+=( ${#DEFER_LIST[@]} ) DEFER_LENGTHS+=( $# ) DEFER_LIST+=( "$@" ) } defer echo a b defer echo "c d" This prints: c d a b The quoted string "c d" is preserved all the way through as one argument to echo. The non-quoted ones work as expected too. Note that since bash doesn't have lists of lists, I've stuck all the word-lists of all the deferred commands together into one giant array, and I've saved offsets and length so I can separate them out again. Since expansion doesn't happen again after you've deferred a command, you can't expand (say) a variable when the deferred command runs. But if you want to do that, define a function and defer it, or just use eval: defer eval 'echo $myvar'