about summary refs log tree commit diff
path: root/Functions/Misc/zargs
blob: b52c80af5a6d844eccd9634ac460c03060c7c39f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# function zargs {
#
# This function works like GNU xargs, except that instead of reading lines
# of arguments from the standard input, it takes them from the command
# line.  This is possible/useful because, especially with recursive glob
# operators, zsh often can construct a command line for a shell function
# that is longer than can be accepted by an external command.
#
# Like xargs, zargs exits with the following status:
#   0 if it succeeds
#   123 if any invocation of the command exited with status 1-125
#   124 if the command exited with status 255
#   125 if the command is killed by a signal
#   126 if the command cannot be run
#   127 if the command is not found
#   1 if some other error occurred.
#
# However, "killed by a signal" is determined by the usual shell rule
# that $? is the signal number plus 128, so zargs can be fooled by a
# command that explicitly exits with 129+.  Also, zsh prior to 4.1.x
# returns 1 rather than 127 for "command not found" so this function
# incorrectly returns 123 in that case if used with zsh 4.0.x.
#
# The full set of GNU xargs options is supported (see help text below);
# although --eof and --max-lines therefore have odd names, they have
# analogous meanings to their xargs counterparts.  Also zargs --help is
# a lot more helpful than xargs --help, at least as of xargs 4.1.
#
# Note that "--" is used both to end the options and to begin the command,
# so to specify some options along with an empty set of input-args, one
# must repeat the "--" as TWO consecutive arguments, e.g.:
#   zargs --verbose -- -- print There are no input-args
# If there is at least one input-arg, the first "--" may be omitted:
#   zargs -p -i one -- print There is just {} input-arg
# Obviously, if there is no command, the second "--" may be omitted:
#   zargs -n2 These words will be echoed in five lines of two
#

emulate -L zsh || return 1
local -a opts eof n s l P i

local ZARGS_VERSION="1.0"

if zparseopts -a opts -D -- \
	-eof::=eof e::=eof \
	-exit x \
	-help \
	-interactive p \
	-max-args:=n n:=n \
	-max-chars:=s s:=s \
	-max-lines::=l l::=l \
	-max-procs:=P P:=P \
	-no-run-if-empty r \
	-null 0 \
	-replace::=i i::=i \
	-verbose t \
	-version
then
    if (( $opts[(I)--version] ))
    then
	print -u2 zargs version $ZARGS_VERSION zsh $ZSH_VERSION
    fi
    if (( $opts[(I)--help] ))
    then
	>&2 <<-\HELP
	Usage: zargs [options --] [input-args] [-- command [initial-args]]

	If command and initial-args are omitted, "print -r --" is used.

	Options:
	--eof[=eof-str], -e[eof-str]
	    Change the end-of-input-args string from "--" to eof-str.  If
	    given as --eof=, an empty argument is the end; as --eof or -e,
	    with no (or an empty) eof-str, all arguments are input-args.
	--exit, -x
	    Exit if the size (see --max-chars) is exceeded.
	--help
	    Print this summary and exit.
	--interactive, -p
	    Prompt before executing each command line.
	--max-args=max-args, -n max-args
	    Use at most max-args arguments per command line.
	--max-chars=max-chars, -s max-chars
	    Use at most max-chars characters per command line.
	--max-lines[=max-lines], -l[max-lines]
	    Use at most max-lines of the input-args per command line.
	    This option is misnamed for xargs compatibility.
	--max-procs=max-procs, -P max-procs
	    Run up to max-procs command lines in the background at once.
	--no-run-if-empty, -r
	    Do nothing if there are no input arguments before the eof-str.
	--null, -0
	    Split each input-arg at null bytes, for xargs compatibility.
	--replace[=replace-str], -i[replace-str]
	    Substitute replace-str in the initial-args by each initial-arg.
	    Implies --exit --max-lines=1.
	--verbose, -t
	    Print each command line to stderr before executing it.
	--version
	    Print the version number of zargs and exit.
HELP
	return 0
    fi
    if (( $opts[(I)--version] ))
    then
	return 0
    fi
    if (( $#i ))
    then
	l=1
	i=${${i##-(i|-replace(=|))}:-\{\}}
	opts[(r)-x]=-x
	# The following is not how xargs is documented,
	# but GNU xargs does behave as if -i implies -r.
	opts[(r)-r]=-r
    fi
else
    return 1
fi

local -i end c=0
if [[ $eof == -(e|-eof) ]]; then ((end=ARGC+1))
elif (( $#eof )); then end=$argv[(i)${eof##-(e|-eof=)}]
else end=$argv[(i)--]
fi
local -a args call command; command=( ${argv[end+1,-1]} )

if (( $opts[(I)-(null|0)] ))
then set -- ${(ps:\000:)argv[1,end-1]}
else set -- $argv[1,end-1]
fi

if [[ -n $command ]]
then (( c = $#command - 1 ))
else command=( print -r -- )
fi

local last='return $ret' execute='
    if (( $opts[(I)-(-interactive|p)] ))
    then read -q "?$call?..." || eval "$last"
    elif (( $opts[(I)-(-verbose|t)] ))
    then print -u2 -r -- "$call"
    fi
    $call
    case $? in
    (0) ;;
    (<1-125>|128)  ret=123;;
    (255)       return 124;;
    (<129-254>) return 125;;
    (126)       return 126;;
    (127)       return 127;;
    (*)         return 1;;
    esac
    eval "$last"'

if (( ARGC == 0 ))
then
    if (( $opts[(I)-(-no-run-if-empty|r)] ))
    then return 0
    else call=($command); eval "$execute"
    fi
fi

n=${${n##-(n|-max-args(=|))}:-$[ARGC+c]}
s=${${s##-(s|-max-chars(=|))}:-20480}
l=${${l##-(l|-max-lines(=|))}:-${${l[1]:+1}:-$ARGC}}
P=${${P##-(P|-max-procs(=|))}:-1}

if (( n > c ))
then (( n -= c ))
else
    print -u2 zargs: argument list too long
    return 1
fi

last='shift $((end > ARGC ? ARGC : end)); continue'
while ((ARGC))
do
    for (( end=l; end && ${(c)#argv[1,end]} > s; end/=2 )) :
    (( end > n && ( end = n ) ))
    args=( $argv[1,end] )
    if (( $#i ))
    then call=( ${command/$i/$args} )
    else call=( $command $args )
    fi
    if (( ${(c)#call} > s ))
    then
	print -u2 zargs: cannot fit single argument within size limit
	# GNU xargs exits here whether or not -x,
	# but that just makes the option useless.
	(( $opts[(I)-(-exit|x)] )) && return 1
	eval "$last"
    else
	eval "$execute"
    fi
done
return $ret

# }