Gentoo Archives: gentoo-dev

From: Aron Griffis <agriffis@×××××××.com>
To: gentoo-dev@g.o
Subject: [gentoo-dev] Thoughts on try()
Date: Wed, 08 Aug 2001 07:42:18
Message-Id: 20010808094133.B320654@zk3.dec.com
1 Hello,
2
3 This email follows a discussion on #gentoo yesterday. I'll start with
4 an executive summary of what follows since this may become long-winded.
5 Feedback and comments are welcome.
6
7 Summary
8 -------
9 try() (in ebuild.sh) attempts to provide ebuilds with an easy
10 mechanism for aborting the build in case a single step fails. Its
11 arguments need to be interpreted as a bash command, including
12 overriding environment variables, bash builtins, and external
13 programs. Quoting needs to be preserved.
14
15 To accomplish this, it needs to use either bash "eval" or the external
16 program "env". However each of these methods has shortcomings, and
17 neither handles pipelines or redirection well. As a result, I propose
18 an alternate method that is more suited to bash syntax: die() and
19 assert_true_or_die().
20
21 Defining the Problem
22 --------------------
23 Ebuild authors need an easy method for "throwing an exception" in an
24 ebuild so that ebuilds can be robust. Non-zero exit status from
25 a command needs to detected, a standard error message given to the
26 user, and the build aborted.
27
28 Constraints:
29
30 (1) The "command" should be written as a normal bash command, with
31 normal quoting, etc. In other words, it should be possible to
32 tack the method onto a command with minimal effort.
33
34 (2) The "command" can be a bash builtin or external program (or
35 function for that matter), and should be able to contain
36 overridden environment variables as bash normally allows.
37
38 Initial solution: try()
39 -----------------------
40 try() was written to follow the pattern of languages with exception
41 handling. It's mission seems simple enough: Interpret the arguments
42 as a bash command, execute the command, abort with an error message if
43 the command fails.
44
45 The problem with try() is the incompatibility of the two constraints
46 in bash. Consider the following list of problems with some
47 skeleton implementations of try():
48
49 * If try() { eval "$@"; } is used, then the arguments to try()
50 must be escaped so that the quoting is preserved.
51
52 * If try() { env "$@"; } is used, then the command can't be a bash
53 builtin, because env doesn't understand bash builtins.
54
55 * If try() { bash -c "$@"; } is used, then the effects of the
56 builtin won't affect the parent shell, e.g. cd and setting of
57 variables.
58
59 * If try() { "$@"; } is used, then bash doesn't evaluate the line
60 for prefixed setting of environment variables.
61
62 However this is only the tip of the iceberg! Here are some more
63 problems that use the following try(), functionally similar to the
64 one in ebuild.sh:
65
66 try() { env "$@"; if [ $? != 0 ]; then echo "ERROR"; exit 1; fi; }
67
68 * Since redirections occur *outside* of the try() function call
69 (programs executed inside of the function inherit the file
70 descriptors), they won't cause try() to fail. For example:
71
72 $ try cat nonexistent_file
73 ERROR (then shell exits)
74
75 $ try cat < nonexistent_file
76 bash: nonexistent_file: No such file or directory
77
78 * Continuing on the redirections thread, consider what happens when
79 you redirect the stderr of the command... The ebuild will bomb
80 out, but the error message will be lost because try() no longer
81 has access to the output terminal. (Yes, it could use /dev/tty,
82 but shouldn't because that would pose problems for automated
83 builds.)
84
85 $ try grep patt * >/dev/null 2>&1
86 [no error message from try]
87
88 * try() only applies to the first element of a pipeline. As
89 a result, the following would not be caught by try().
90
91 $ try cat bogus_but_existing_patch | patch -p1
92 patch: **** Only garbage was found in the patch input.
93
94 I think try() is a good idea, but the limitations of bash syntax make
95 it difficult to implement well. Therefore I propose a different
96 solution...
97
98 Proposed replacement: die() and assert_true_or_die()
99 ----------------------------------------------------
100 diefunc() {
101 local funcname="$1" lineno="$2" exitcode="$3"
102 shift 3
103 echo &>2
104 echo "!!! ERROR: The ebuild did not complete successfully." &>2
105 echo "!!! Function $funcname, Line $lineno, Exitcode $exitcode" &>2
106 echo "!!! ${*:-(no error message)}" &>2
107 echo &>2
108 exit 1
109 }
110 alias die='diefunc "$FUNCNAME" "$LINENO" "$?"'
111 alias assert_true_or_die='_retval=$?; [ $_retval = 0 ] || \
112 diefunc "$FUNCNAME" "$LINENO" "$_retval"'
113
114 The aliases are necessary to get FUNCNAME and LINENO from the
115 caller rather than the callee (diefunc).
116
117 Note one thing (other than usage) that changes between try() and
118 die(). The error output of try() shows the actual command that was
119 attempted. That isn't possible with die() because it isn't being
120 passed the command. However it attempts to make up the shortcoming by
121 including the function name, the line number, the exitcode, and
122 (bonus) a custom message that can be passed by the caller.
123
124 To convert try-lines to die-lines would be as follows (each line
125 followed by its conversion). These examples are all from the Linux
126 kernel ebuild.
127
128 try mv linux linux-${KV}
129
130 mv linux linux-${KV} || die
131
132 ----------------------------------------
133
134 try bzip2 -dc ${DISTDIR}/patch-${KV}.bz2 | patch -p1
135
136 bzip2 -dc ${DISTDIR}/patch-${KV}.bz2 | patch -p1 || die
137
138 # or for checking the first half of the pipeline as well as the
139 # second half, since bash only reports exit status from the last
140 # element on a pipeline...
141 (bzip2 -dc ${DISTDIR}/patch-${KV}.bz2 || die) | patch -p1 || die
142
143 ----------------------------------------
144
145 try cd LVM/${LVMV}
146
147 # include a custom message
148 cd LVM/${LVMV} || die "Looks like LVM didn't unpack correctly."
149
150 ----------------------------------------
151
152 try CFLAGS="${CFLAGS} -I${S}/include" ./configure --prefix=/ \
153 --mandir=/usr/share/man --with-kernel_dir="${S}"
154
155 # use the assert_true_or_die() version for clarity
156 CFLAGS="${CFLAGS} -I${S}/include" ./configure --prefix=/ \
157 --mandir=/usr/share/man --with-kernel_dir="${S}"
158 assert_true_or_die "Kernel configuration failed"
159
160 ----------------------------------------
161
162 try make KERNEL_VERSION=${KV} KERNEL_DIR=${S}
163
164 make KERNEL_VERSION=${KV} KERNEL_DIR=${S} || die
165
166 Conclusion
167 ----------
168 I believe that die() and assert_true_or_die() fulfill the constraints,
169 provide a workable replacement for try(), and are more bashish all
170 around. Note there is no need to immediately convert all existing
171 ebuilds to die() and assert_true_or_die(). If this proposal is
172 accepted (after discussion and potential modifications), then the new
173 functions should be used in new ebuilds, and old ebuilds can be
174 gradually changed to the new scheme.
175
176 Feedback/questions/comments are welcome! Thanks for reading.
177
178 Aron

Replies

Subject Author
Re: [gentoo-dev] Thoughts on try() Daniel Robbins <drobbins@g.o>
Re: [gentoo-dev] Thoughts on try() Aron Griffis <agriffis@×××××××.com>