output asked headers in the order they were asked; avoid header name spoofing by...
[hband-tools.git] / user-tools / foreach
blob7536d9ccb207238fb0e8c00ab8811d408e5fff92
1 #!/bin/bash
3 true <<'EOF'
4 =pod
6 =head1 NAME
8 foreach - Run an OS or shell command on each input line, similar to xargs(1)
10 =head1 SYNOPSIS
12 foreach [I<OPTIONS>] I<COMMAND> [I<ARGS> ...]
14 =head1 DESCRIPTION
16 Take each input line from stdin as B<DATA>, and run I<COMMAND> with B<DATA> appended to the end of I<ARGS> as a single argument.
18 If C<{}> is there in I<ARGS> then it's substituted with B<DATA> rather than append to the end,
19 unless I<--no-placeholder> is given, because then C<{}> is read literally.
20 Additionally, parse B<DATA> into fields and add each of them to the end of I<ARGS> if I<--fields> is given.
21 Numbered placeholders, like C<{0}>, C<{1}>, ... are substituted with the respective field's value.
23 So if you have not specified any I<ARGS> in the command line and type both I<--data> and I<--fields>,
24 then B<DATA> goes into argv[1], and the first field goes into argv[2], second to argv[3] and so on.
25 If have not given I<--data> nor I<--fields>, then I<--data> is implied.
27 If called with I<--sh> option, I<COMMAND> is run within a shell context;
28 input line goes to C<$DATA>, individual fields go to C<${FIELD[@]}> (0-indexed).
30 Both in command and shell (--sh) modes, individual fields are available in
31 C<$F0>, C<$F1>, ... environment variables.
33 Set I<-d> B<DELIM> if you want to split B<DATA> not by C<$IFS> but by other delimiter chars,
34 eg. C<-d ",:"> for command and colon.
35 There is also C<-t>/C<--tab> option to set delimiter to TAB for your convenience.
37 =head1 OPTIONS
39 =over 4
41 =item -e, --sh
43 I<COMMAND> is a shell script and for each B<DATA>, it runs in the same shell context,
44 so variables are preserved across invocations.
46 =item -l, --data
48 Pass B<DATA> in the arguments after the user-specified I<ARGS>.
50 =item -f, --fields
52 Pass individual fields of B<DATA> in the arguments after B<DATA> if I<--data> is given,
53 or after the user-specified I<ARGS> if I<--data> is not given.
55 =item -d, --delimiter DELIM
57 Cut up B<DATA> into fields at B<DELIM> chars.
58 Default is C<$IFS>.
60 =item -t, --tab
62 Cut up B<DATA> into fields at TAB chars..
64 =item -P, --no-placeholder
66 Do not substitute C<{}> with B<DATA>.
68 =item -p, --prefix I<TEMPLATE>
70 Print something before each command execution.
71 I<TEMPLATE> is a bash-interpolated string,
72 may contain C<$DATA> and C<${FIELD[n]}>.
73 Probably need to put in single quotes when passing to foreach(1) in the invoking shell.
74 Beware using backtick, command substitution, double quotes, semicolon - since it's B<eval>ed.
76 =item -v, --verbose
78 =item -n, --dry-run
80 =item -E, --errexit
82 Stop executing if a I<COMMAND> returns non-zero.
83 Rather exit with the said command's exit status code.
85 =back
87 =head1 EXAMPLES
89 ls -l --time-style +%FT%T%z | foreach --data --fields sh -c 'echo size: $5, file: $7'
90 ls -l --time-style +%FT%T%z | foreach --sh 'echo size: ${FIELD[4]}, file: ${FIELD[6]}'
92 =head1 LIMITS
94 Placeholders for field values (C<{0}>, C<{1}>, ...) are considered from 0 up to 99.
95 There must be a limit somewhere, otherwise I had to write a more complex replace routine.
97 =head1 CAVEATS
99 Placeholder C<{}> is substituted in all I<ARGS> anywhere, not just stand-alone C<{}> arguments,
100 but IS NOT ESCAPED!
101 So be careful using it in shell command arguments like C<sh -e 'echo "data is: {}"'>.
103 =head1 SEE ALSO
105 xargs(1), xe(1), apply(1), xapply(1) L<https://www.databits.net/~ksb/msrc/local/bin/xapply/xapply.html>
107 =cut
112 # syntax highlighter correction: '
116 FOREACH_MODE=command
117 FOREACH_VERBOSE=no
118 unset FOREACH_DELIM
119 FOREACH_PASS_DATA=maybe
120 FOREACH_PASS_FIELDS=no
121 FOREACH_REPLACE_PLACEHOLDER=yes
122 FOREACH_DRYRUN=no
123 FOREACH_ERREXIT=no
124 FOREACH_PREFIX_TEMPLATE=''
127 while [ $# != 0 ]
129 case "$1" in
130 --help)
131 pod2text "$0"
132 exit
134 -e|--sh)
135 FOREACH_MODE=sh
137 -l|--data)
138 FOREACH_PASS_DATA=yes
140 -f|--fields)
141 FOREACH_PASS_FIELDS=yes
143 -d|--delimiter)
144 shift
145 FOREACH_DELIM=$1
147 -t|--tab)
148 FOREACH_DELIM=$'\t'
150 -P|--no-placeholder)
151 FOREACH_REPLACE_PLACEHOLDER=no
153 -p|--prefix)
154 shift
155 FOREACH_PREFIX_TEMPLATE=$1
157 -v|--verbose)
158 FOREACH_VERBOSE=yes
160 -n|--dry-run)
161 FOREACH_DRYRUN=yes
163 -E|--errexit)
164 FOREACH_ERREXIT=yes
167 shift
168 break;;
170 echo "foreach: unknown option: $1" >&2
171 exit -1
173 *) break;;
174 esac
175 shift
176 done
178 if [ $# = 0 ]
179 then
180 pod2text "$0" >&2
181 exit -1
184 if [ $FOREACH_PASS_FIELDS = yes -a $FOREACH_PASS_DATA != yes ]
185 then
186 FOREACH_PASS_DATA=no
188 if [ $FOREACH_PASS_FIELDS != yes ]
189 then
190 FOREACH_PASS_DATA=yes
193 declare -a FOREACH_COMMAND=("$@")
195 while read -r DATA
197 export DATA
198 declare -a FIELD=()
199 IFS=${FOREACH_DELIM+$FOREACH_DELIM}${FOREACH_DELIM-$IFS} read -r -a FIELD <<< "$DATA"
201 for ((idx = 0; idx < ${#FIELD[@]}; idx++))
203 export F$idx="${FIELD[$idx]}"
204 done
205 # unset old F$idx variables
206 while declare -p F$idx >/dev/null 2>/dev/null
208 unset F$idx
209 let idx++
210 done
212 FOREACH_COMMAND_STATUS=0
214 declare -a cmd_and_args=()
215 placeholder_found=no
216 if [ "$FOREACH_REPLACE_PLACEHOLDER" = yes ]
217 then
218 for arg in "${FOREACH_COMMAND[@]}"
220 if [[ $arg =~ \{\} ]]
221 then
222 placeholder='{}'
223 arg=${arg//$placeholder/$DATA}
224 placeholder_found=yes
226 for ((idx = 0; idx < 100; idx++))
228 if [[ $arg =~ \{$idx\} ]]
229 then
230 placeholder="{$idx}"
231 arg=${arg//$placeholder/${FIELD[$idx]}}
232 placeholder_found=yes
234 done
235 cmd_and_args+=("$arg")
236 done
237 else
238 cmd_and_args=("${FOREACH_COMMAND[@]}")
241 if [ "$FOREACH_MODE" = command ]
242 then
243 if [ "$FOREACH_REPLACE_PLACEHOLDER" != yes -o $placeholder_found = no ]
244 then
245 if [ "$FOREACH_PASS_DATA" = yes ]; then cmd_and_args+=("$DATA"); fi
246 if [ "$FOREACH_PASS_FIELDS" = yes ]; then cmd_and_args+=("${FIELD[@]}"); fi
250 if [ "$FOREACH_VERBOSE" = yes ]
251 then
252 echo "foreach: ${cmd_and_args[*]}" >&2
255 eval "FOREACH_PREFIX=\"$FOREACH_PREFIX_TEMPLATE\""
256 echo -n "$FOREACH_PREFIX"
258 if [ "$FOREACH_DRYRUN" = no ]
259 then
260 if [ "$FOREACH_MODE" = command ]
261 then
262 command "${cmd_and_args[@]}"
263 FOREACH_COMMAND_STATUS=$?
264 else
265 eval "${cmd_and_args[@]}"
266 FOREACH_COMMAND_STATUS=$?
270 if [ $FOREACH_ERREXIT = yes -a $FOREACH_COMMAND_STATUS != 0 ]
271 then
272 exit $status
274 done