def _Read(self, lex_mode): # type: (lex_mode_t) -> Token """Read from the normal line buffer, not an alias.""" t = self.line_lexer.Read(lex_mode) if t.id == Id.Eol_Tok: # hit \0, read a new line line_id, line, line_pos = self.line_reader.GetLine() if line is None: # no more lines span_id = self.line_lexer.GetSpanIdForEof() if self.emit_comp_dummy: id_ = Id.Lit_CompDummy self.emit_comp_dummy = False # emit EOF the next time else: id_ = Id.Eof_Real t = Token(id_, span_id, '') return t self.line_lexer.Reset(line, line_id, line_pos) # fill with a new line t = self.line_lexer.Read(lex_mode) # e.g. translate ) or ` into EOF if len(self.translation_stack): old_id, new_id = self.translation_stack[-1] # top if t.id == old_id: #print('==> TRANSLATING %s ==> %s' % (t, new_s)) self.translation_stack.pop() t.id = new_id return t
def Run(self, cmd_val): # type: (cmd_value__Argv) -> int argv = cmd_val.argv[1:] attrs, arg_r = flag_spec.ParseLikeEcho('echo', cmd_val) arg = arg_types.echo(attrs.attrs) argv = arg_r.Rest() backslash_c = False # \c terminates input arg0_spid = cmd_val.arg_spids[0] if arg.e: new_argv = [] # type: List[str] for a in argv: parts = [] # type: List[str] lex = match.EchoLexer(a) while not backslash_c: id_, value = lex.Next() if id_ == Id.Eol_Tok: # Note: This is really a NUL terminator break tok = Token(id_, arg0_spid, value) p = word_compile.EvalCStringToken(tok) # Unusual behavior: '\c' prints what is there and aborts processing! if p is None: backslash_c = True break parts.append(p) new_argv.append(''.join(parts)) if backslash_c: # no more args either break # Replace it argv = new_argv if self.exec_opts.simple_echo(): n = len(argv) if n == 0: pass elif n == 1: self.f.write(argv[0]) else: # TODO: span_id could be more accurate e_usage( "takes at most one arg when simple_echo is on (hint: add quotes)" ) else: #log('echo argv %s', argv) for i, a in enumerate(argv): if i != 0: self.f.write(' ') # arg separator self.f.write(a) if not arg.n and not backslash_c: self.f.write('\n') return 0
def RunCommandSub(self, node): # type: (command_t) -> str # Hack for weird $(<file) construct if node.tag_() == command_e.Simple: simple = cast(command__Simple, node) # Detect '< file' if (len(simple.words) == 0 and len(simple.redirects) == 1 and simple.redirects[0].op.id == Id.Redir_Less): # change it to __cat < file # note: cmd_eval.py _Dispatch works around lack of spid tok = Token(Id.Lit_Chars, runtime.NO_SPID, '__cat') cat_word = compound_word([tok]) # MUTATE the command.Simple node. This will only be done the first # time in the parent process. simple.words.append(cat_word) p = self._MakeProcess(node, inherit_errexit=self.exec_opts.inherit_errexit()) r, w = posix.pipe() p.AddStateChange(process.StdoutToPipe(r, w)) _ = p.Start() #log('Command sub started %d', pid) chunks = [] # type: List[str] posix.close(w) # not going to write while True: byte_str = posix.read(r, 4096) if len(byte_str) == 0: break chunks.append(byte_str) posix.close(r) status = p.Wait(self.waiter) # OSH has the concept of aborting in the middle of a WORD. We're not # waiting until the command is over! if self.exec_opts.more_errexit(): if self.exec_opts.errexit() and status != 0: raise error.ErrExit('Command sub exited with status %d (%r)', status, NewStr(command_str(node.tag_()))) else: # Set a flag so we check errexit at the same time as bash. Example: # # a=$(false) # echo foo # no matter what comes here, the flag is reset # # Set ONLY until this command node has finished executing. # HACK: move this self.cmd_ev.check_command_sub_status = True self.mem.SetLastStatus(status) # Runtime errors test case: # $("echo foo > $@") # Why rstrip()? # https://unix.stackexchange.com/questions/17747/why-does-shell-command-substitution-gobble-up-a-trailing-newline-char return ''.join(chunks).rstrip('\n')
def _ExpandPart( parts, # type: List[word_part_t] first_alt_index, # type: int suffixes, # type: List[List[word_part_t]] ): # type: (...) -> List[List[word_part_t]] """Mutually recursive with _BraceExpand. Args: parts: input parts first_alt_index: index of the first BracedTuple suffixes: List of suffixes to append. """ out = [] # type: List[List[word_part_t]] prefix = parts[:first_alt_index] expand_part = parts[first_alt_index] UP_part = expand_part with tagswitch(expand_part) as case: if case(word_part_e.BracedTuple): expand_part = cast(word_part__BracedTuple, UP_part) # Call _BraceExpand on each of the inner words too! expanded_alts = [] # type: List[List[word_part_t]] for w in expand_part.words: expanded_alts.extend(_BraceExpand(w.parts)) for alt_parts in expanded_alts: for suffix in suffixes: out_parts = [] # type: List[word_part_t] out_parts.extend(prefix) out_parts.extend(alt_parts) out_parts.extend(suffix) out.append(out_parts) elif case(word_part_e.BracedRange): expand_part = cast(word_part__BracedRange, UP_part) # Not mutually recursive with _BraceExpand strs = _RangeStrings(expand_part) for s in strs: for suffix in suffixes: out_parts_ = [] # type: List[word_part_t] out_parts_.extend(prefix) # Preserve span_id from the original t = Token(Id.Lit_Chars, expand_part.spids[0], s) out_parts_.append(t) out_parts_.extend(suffix) out.append(out_parts_) else: raise AssertionError() return out
def Read(self, lex_mode): # type: (lex_mode_t) -> Token # Inner loop optimization line = self.line line_pos = self.line_pos tok_type, end_pos = match.OneToken(lex_mode, line, line_pos) if tok_type == Id.Eol_Tok: # Do NOT add a span for this sentinel! return _EOL_TOK # Save on allocations! We often don't look at the token value. # TODO: can inline this function with formula on 16-bit Id. kind = consts.GetKind(tok_type) # Whitelist doesn't work well? Use blacklist for now. # - Kind.KW is sometimes a literal in a word # - Kind.Right is for " in here docs. Lexer isn't involved. # - Got an error with Kind.Left too that I don't understand # if kind in (Kind.Lit, Kind.VSub, Kind.Redir, Kind.Char, Kind.Backtick, Kind.KW, Kind.Right): if kind in (Kind.Arith, Kind.Op, Kind.WS, Kind.Ignored, Kind.Eof): tok_val = None # type: Optional[str] else: tok_val = line[line_pos:end_pos] # NOTE: We're putting the arena hook in LineLexer and not Lexer because we # want it to be "low level". The only thing fabricated here is a newline # added at the last line, so we don't end with \0. if self.arena_skip: # make another token from the last span assert self.last_span_id != runtime.NO_SPID span_id = self.last_span_id self.arena_skip = False else: tok_len = end_pos - line_pos span_id = self.arena.AddLineSpan(self.line_id, line_pos, tok_len) self.last_span_id = span_id #log('LineLexer.Read() span ID %d for %s', span_id, tok_type) t = Token(tok_type, span_id, tok_val) self.line_pos = end_pos return t
def Tok(id_, val): return Token(id_, runtime.NO_SPID, val)
from _devbuild.gen.types_asdl import lex_mode_t from _devbuild.gen.id_kind_asdl import Id_t, Id, Kind from asdl import runtime from core.pyerror import log from mycpp import mylib from frontend import consts from frontend import match from typing import Callable, List, Tuple, Optional, Counter, TYPE_CHECKING if TYPE_CHECKING: from core.alloc import Arena from core import optview from frontend.reader import _Reader # Special immutable tokens _EOL_TOK = Token(Id.Eol_Tok, runtime.NO_SPID, None) class LineLexer(object): def __init__(self, line, arena): # type: (str, Arena) -> None self.arena = arena self.arena_skip = False # For MaybeUnreadOne self.last_span_id = runtime.NO_SPID # For MaybeUnreadOne self.Reset(line, -1, 0) # Invalid line_id to start def __repr__(self): # type: () -> str return '<LineLexer at pos %d of line %r (id = %d)>' % (
def Run(self, cmd_val): # type: (cmd_value__Argv) -> int """ printf: printf [-v var] format [argument ...] """ attrs, arg_r = flag_spec.ParseCmdVal('printf', cmd_val) arg = arg_types.printf(attrs.attrs) fmt, fmt_spid = arg_r.ReadRequired2('requires a format string') varargs, spids = arg_r.Rest2() #log('fmt %s', fmt) #log('vals %s', vals) arena = self.parse_ctx.arena if fmt in self.parse_cache: parts = self.parse_cache[fmt] else: line_reader = reader.StringLineReader(fmt, arena) # TODO: Make public lexer = self.parse_ctx._MakeLexer(line_reader) parser = _FormatStringParser(lexer) with alloc.ctx_Location(arena, source.ArgvWord(fmt_spid)): try: parts = parser.Parse() except error.Parse as e: self.errfmt.PrettyPrintError(e) return 2 # parse error self.parse_cache[fmt] = parts if 0: print() for part in parts: part.PrettyPrint() print() out = [] # type: List[str] arg_index = 0 num_args = len(varargs) backslash_c = False while True: for part in parts: UP_part = part if part.tag_() == printf_part_e.Literal: part = cast(printf_part__Literal, UP_part) token = part.token if token.id == Id.Format_EscapedPercent: s = '%' else: s = word_compile.EvalCStringToken(token) out.append(s) elif part.tag_() == printf_part_e.Percent: part = cast(printf_part__Percent, UP_part) flags = [] # type: List[str] if len(part.flags) > 0: for flag_token in part.flags: flags.append(flag_token.val) width = -1 # nonexistent if part.width: if part.width.id in (Id.Format_Num, Id.Format_Zero): width_str = part.width.val width_spid = part.width.span_id elif part.width.id == Id.Format_Star: if arg_index < num_args: width_str = varargs[arg_index] width_spid = spids[arg_index] arg_index += 1 else: width_str = '' # invalid width_spid = runtime.NO_SPID else: raise AssertionError() try: width = int(width_str) except ValueError: if width_spid == runtime.NO_SPID: width_spid = part.width.span_id self.errfmt.Print_("printf got invalid width %r" % width_str, span_id=width_spid) return 1 precision = -1 # nonexistent if part.precision: if part.precision.id == Id.Format_Dot: precision_str = '0' precision_spid = part.precision.span_id elif part.precision.id in (Id.Format_Num, Id.Format_Zero): precision_str = part.precision.val precision_spid = part.precision.span_id elif part.precision.id == Id.Format_Star: if arg_index < num_args: precision_str = varargs[arg_index] precision_spid = spids[arg_index] arg_index += 1 else: precision_str = '' precision_spid = runtime.NO_SPID else: raise AssertionError() try: precision = int(precision_str) except ValueError: if precision_spid == runtime.NO_SPID: precision_spid = part.precision.span_id self.errfmt.Print_( 'printf got invalid precision %r' % precision_str, span_id=precision_spid) return 1 #log('index=%d n=%d', arg_index, num_args) if arg_index < num_args: s = varargs[arg_index] word_spid = spids[arg_index] arg_index += 1 else: s = '' word_spid = runtime.NO_SPID typ = part.type.val if typ == 's': if precision >= 0: s = s[:precision] # truncate elif typ == 'q': s = qsn.maybe_shell_encode(s) elif typ == 'b': # Process just like echo -e, except \c handling is simpler. c_parts = [] # type: List[str] lex = match.EchoLexer(s) while True: id_, tok_val = lex.Next() if id_ == Id.Eol_Tok: # Note: This is really a NUL terminator break # TODO: add span_id from argv tok = Token(id_, runtime.NO_SPID, tok_val) p = word_compile.EvalCStringToken(tok) # Unusual behavior: '\c' aborts processing! if p is None: backslash_c = True break c_parts.append(p) s = ''.join(c_parts) elif typ in 'diouxX' or part.type.id == Id.Format_Time: try: d = int(s) except ValueError: if len(s) >= 1 and s[0] in '\'"': # TODO: utf-8 decode s[1:] to be more correct. Probably # depends on issue #366, a utf-8 library. # Note: len(s) == 1 means there is a NUL (0) after the quote.. d = ord(s[1]) if len(s) >= 2 else 0 elif part.type.id == Id.Format_Time and len( s) == 0 and word_spid == runtime.NO_SPID: # Note: No argument means -1 for %(...)T as in Bash Reference # Manual 4.2 "If no argument is specified, conversion behaves # as if -1 had been given." d = -1 else: if word_spid == runtime.NO_SPID: # Blame the format string blame_spid = part.type.span_id else: blame_spid = word_spid self.errfmt.Print_( 'printf expected an integer, got %r' % s, span_id=blame_spid) return 1 if typ in 'di': s = str(d) elif typ in 'ouxX': if d < 0: e_die( "Can't format negative number %d with %%%s", d, typ, span_id=part.type.span_id) if typ == 'u': s = str(d) elif typ == 'o': s = mylib.octal(d) elif typ == 'x': s = mylib.hex_lower(d) elif typ == 'X': s = mylib.hex_upper(d) elif part.type.id == Id.Format_Time: # %(...)T # Initialize timezone: # `localtime' uses the current timezone information initialized # by `tzset'. The function `tzset' refers to the environment # variable `TZ'. When the exported variable `TZ' is present, # its value should be reflected in the real environment # variable `TZ' before call of `tzset'. # # Note: unlike LANG, TZ doesn't seem to change behavior if it's # not exported. # # TODO: In Oil, provide an API that doesn't rely on libc's # global state. tzcell = self.mem.GetCell('TZ') if tzcell and tzcell.exported and tzcell.val.tag_( ) == value_e.Str: tzval = cast(value__Str, tzcell.val) posix.putenv('TZ', tzval.s) time_.tzset() # Handle special values: # User can specify two special values -1 and -2 as in Bash # Reference Manual 4.2: "Two special argument values may be # used: -1 represents the current time, and -2 represents the # time the shell was invoked." from # https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-printf if d == -1: # the current time ts = time_.time() elif d == -2: # the shell start time ts = self.shell_start_time else: ts = d s = time_.strftime(typ[1:-2], time_.localtime(ts)) if precision >= 0: s = s[:precision] # truncate else: raise AssertionError() else: raise AssertionError() if width >= 0: if len(flags): if '-' in flags: s = s.ljust(width, ' ') elif '0' in flags: s = s.rjust(width, '0') else: pass else: s = s.rjust(width, ' ') out.append(s) else: raise AssertionError() if backslash_c: # 'printf %b a\cb xx' - \c terminates processing! break if arg_index >= num_args: break # Otherwise there are more args. So cycle through the loop once more to # implement the 'arg recycling' behavior. result = ''.join(out) if arg.v is not None: # TODO: get the span_id for arg.v! v_spid = runtime.NO_SPID arena = self.parse_ctx.arena a_parser = self.parse_ctx.MakeArithParser(arg.v) with alloc.ctx_Location(arena, source.ArgvWord(v_spid)): try: anode = a_parser.Parse() except error.Parse as e: ui.PrettyPrintError(e, arena) # show parse error e_usage('Invalid -v expression', span_id=v_spid) lval = self.arith_ev.EvalArithLhs(anode, v_spid) if not self.exec_opts.eval_unsafe_arith( ) and lval.tag_() != lvalue_e.Named: e_usage( '-v expected a variable name. shopt -s eval_unsafe_arith allows expressions', span_id=v_spid) state.SetRef(self.mem, lval, value.Str(result)) else: mylib.Stdout().write(result) return 0
def ErrorWord(error_str): # type: (str) -> compound_word t = Token(Id.Lit_Chars, runtime.NO_SPID, error_str) return compound_word([t])
def ErrorWord(fmt, err): # type: (str, _ErrorWithLocation) -> compound_word error_str = fmt % err.UserErrorString() t = Token(Id.Lit_Chars, runtime.NO_SPID, error_str) return compound_word([t])
def RunCommandSub(self, cs_part): # type: (command_sub) -> str if not self.exec_opts.allow_command_sub(): # TODO: # - Add spid of $( # - Better hints. Use 'run' for 'if myfunc', and 2 lines like local x; # x=$(false) fo assignment builtins. # - Maybe we should have an error message ID that links somewhere? e_die( "Command subs not allowed here because status wouldn't be checked (strict_errexit)." ) node = cs_part.child # Hack for weird $(<file) construct if node.tag_() == command_e.Simple: simple = cast(command__Simple, node) # Detect '< file' if (len(simple.words) == 0 and len(simple.redirects) == 1 and simple.redirects[0].op.id == Id.Redir_Less): # change it to __cat < file # note: cmd_eval.py _Dispatch works around lack of spid tok = Token(Id.Lit_Chars, runtime.NO_SPID, '__cat') cat_word = compound_word([tok]) # MUTATE the command.Simple node. This will only be done the first # time in the parent process. simple.words.append(cat_word) p = self._MakeProcess(node, inherit_errexit=self.exec_opts.inherit_errexit()) r, w = posix.pipe() p.AddStateChange(process.StdoutToPipe(r, w)) _ = p.Start() #log('Command sub started %d', pid) chunks = [] # type: List[str] posix.close(w) # not going to write while True: byte_str = posix.read(r, 4096) if len(byte_str) == 0: break chunks.append(byte_str) posix.close(r) status = p.Wait(self.waiter) # OSH has the concept of aborting in the middle of a WORD. We're not # waiting until the command is over! if self.exec_opts.command_sub_errexit(): if status != 0: raise error.ErrExit('Command sub exited with status %d (%s)' % (status, ui.CommandType(node)), span_id=cs_part.left_token.span_id, status=status) else: # Set a flag so we check errexit at the same time as bash. Example: # # a=$(false) # echo foo # no matter what comes here, the flag is reset # # Set ONLY until this command node has finished executing. # HACK: move this self.cmd_ev.check_command_sub_status = True self.mem.SetLastStatus(status) # Runtime errors test case: # $("echo foo > $@") # Why rstrip()? # https://unix.stackexchange.com/questions/17747/why-does-shell-command-substitution-gobble-up-a-trailing-newline-char return ''.join(chunks).rstrip('\n')
def _Format(self, parts, varargs, spids, out): # type: (List[printf_part_t], List[str], List[int], List[str]) -> int """Hairy printf formatting logic.""" arg_index = 0 num_args = len(varargs) backslash_c = False while True: # loop over arguments for part in parts: # loop over parsed format string UP_part = part if part.tag_() == printf_part_e.Literal: part = cast(printf_part__Literal, UP_part) token = part.token if token.id == Id.Format_EscapedPercent: s = '%' else: s = word_compile.EvalCStringToken(token) out.append(s) elif part.tag_() == printf_part_e.Percent: # Note: This case is very long, but hard to refactor because of the # error cases and "recycling" of args! (arg_index, return 1, etc.) part = cast(printf_part__Percent, UP_part) # TODO: These calculations are independent of the data, so could be # cached flags = [] # type: List[str] if len(part.flags) > 0: for flag_token in part.flags: flags.append(flag_token.val) width = -1 # nonexistent if part.width: if part.width.id in (Id.Format_Num, Id.Format_Zero): width_str = part.width.val width_spid = part.width.span_id elif part.width.id == Id.Format_Star: if arg_index < num_args: width_str = varargs[arg_index] width_spid = spids[arg_index] arg_index += 1 else: width_str = '' # invalid width_spid = runtime.NO_SPID else: raise AssertionError() try: width = int(width_str) except ValueError: if width_spid == runtime.NO_SPID: width_spid = part.width.span_id self.errfmt.Print_("printf got invalid width %r" % width_str, span_id=width_spid) return 1 precision = -1 # nonexistent if part.precision: if part.precision.id == Id.Format_Dot: precision_str = '0' precision_spid = part.precision.span_id elif part.precision.id in (Id.Format_Num, Id.Format_Zero): precision_str = part.precision.val precision_spid = part.precision.span_id elif part.precision.id == Id.Format_Star: if arg_index < num_args: precision_str = varargs[arg_index] precision_spid = spids[arg_index] arg_index += 1 else: precision_str = '' precision_spid = runtime.NO_SPID else: raise AssertionError() try: precision = int(precision_str) except ValueError: if precision_spid == runtime.NO_SPID: precision_spid = part.precision.span_id self.errfmt.Print_( 'printf got invalid precision %r' % precision_str, span_id=precision_spid) return 1 if arg_index < num_args: s = varargs[arg_index] word_spid = spids[arg_index] arg_index += 1 has_arg = True else: s = '' word_spid = runtime.NO_SPID has_arg = False typ = part.type.val if typ == 's': if precision >= 0: s = s[:precision] # truncate elif typ == 'q': # TODO: most shells give \' for single quote, while OSH gives $'\'' # this could matter when SSH'ing s = qsn.maybe_shell_encode(s) elif typ == 'b': # Process just like echo -e, except \c handling is simpler. c_parts = [] # type: List[str] lex = match.EchoLexer(s) while True: id_, tok_val = lex.Next() if id_ == Id.Eol_Tok: # Note: This is really a NUL terminator break # TODO: add span_id from argv tok = Token(id_, runtime.NO_SPID, tok_val) p = word_compile.EvalCStringToken(tok) # Unusual behavior: '\c' aborts processing! if p is None: backslash_c = True break c_parts.append(p) s = ''.join(c_parts) elif part.type.id == Id.Format_Time or typ in 'diouxX': # %(...)T and %d share this complex integer conversion logic try: d = int( s ) # note: spaces like ' -42 ' accepted and normalized except ValueError: # 'a is interpreted as the ASCII value of 'a' if len(s) >= 1 and s[0] in '\'"': # TODO: utf-8 decode s[1:] to be more correct. Probably # depends on issue #366, a utf-8 library. # Note: len(s) == 1 means there is a NUL (0) after the quote.. d = ord(s[1]) if len(s) >= 2 else 0 # No argument means -1 for %(...)T as in Bash Reference Manual # 4.2 "If no argument is specified, conversion behaves as if -1 # had been given." elif not has_arg and part.type.id == Id.Format_Time: d = -1 else: blame_spid = word_spid if has_arg else part.type.span_id self.errfmt.Print_( 'printf expected an integer, got %r' % s, span_id=blame_spid) return 1 if part.type.id == Id.Format_Time: # Initialize timezone: # `localtime' uses the current timezone information initialized # by `tzset'. The function `tzset' refers to the environment # variable `TZ'. When the exported variable `TZ' is present, # its value should be reflected in the real environment # variable `TZ' before call of `tzset'. # # Note: unlike LANG, TZ doesn't seem to change behavior if it's # not exported. # # TODO: In Oil, provide an API that doesn't rely on libc's # global state. tzcell = self.mem.GetCell('TZ') if tzcell and tzcell.exported and tzcell.val.tag_( ) == value_e.Str: tzval = cast(value__Str, tzcell.val) posix.putenv('TZ', tzval.s) time_.tzset() # Handle special values: # User can specify two special values -1 and -2 as in Bash # Reference Manual 4.2: "Two special argument values may be # used: -1 represents the current time, and -2 represents the # time the shell was invoked." from # https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-printf if d == -1: # the current time ts = time_.time() elif d == -2: # the shell start time ts = self.shell_start_time else: ts = d s = time_.strftime(typ[1:-2], time_.localtime(ts)) if precision >= 0: s = s[:precision] # truncate else: # typ in 'diouxX' # Disallowed because it depends on 32- or 64- bit if d < 0 and typ in 'ouxX': e_die( "Can't format negative number %d with %%%s", d, typ, span_id=part.type.span_id) if typ == 'o': s = mylib.octal(d) elif typ == 'x': s = mylib.hex_lower(d) elif typ == 'X': s = mylib.hex_upper(d) else: # diu s = str(d) # without spaces like ' -42 ' # There are TWO different ways to ZERO PAD, and they differ on # the negative sign! See spec/builtin-printf zero_pad = 0 # no zero padding if width >= 0 and '0' in flags: zero_pad = 1 # style 1 elif precision > 0 and len(s) < precision: zero_pad = 2 # style 2 if zero_pad: negative = (s[0] == '-') if negative: digits = s[1:] sign = '-' if zero_pad == 1: # [%06d] -42 becomes [-00042] (6 TOTAL) n = width - 1 else: # [%6.6d] -42 becomes [-000042] (1 for '-' + 6) n = precision else: digits = s sign = '' if zero_pad == 1: n = width else: n = precision s = sign + digits.rjust(n, '0') else: raise AssertionError() if width >= 0: if '-' in flags: s = s.ljust(width, ' ') else: s = s.rjust(width, ' ') out.append(s) else: raise AssertionError() if backslash_c: # 'printf %b a\cb xx' - \c terminates processing! break if arg_index >= num_args: break # Otherwise there are more args. So cycle through the loop once more to # implement the 'arg recycling' behavior. return 0