Cmd parse error

Hello,

Hello,

I’m hoping someone can point me in the right direction. I need to call an elevated cmd to copy a folder from a specified location to another. Sadly the Target location tends to need admin rights to modify.

 
string CmdString = "xcopy /I /E"+ " " + """ + SourcePath +"""+" "+"""+ TargetPath +""";

ProcessStartInfo copy = new ProcessStartInfo("cmd.exe"); 
                copy.UseShellExecute = true;
                copy.Verb = "runas";
                copy.Arguments = "/K" + """+ CmdString + """; // /K Goes There Apparently.
                Process.Start(copy);

Whenever this is ran an elevated cmd prompt appears correctly but all it displays is «parse error» and waits for the next command. If I write my CmdString string to console and copy it directly into the cmd window it works exactly as expected.

I’m terribly sorry if this is an obvious answer. I have been searching for a couple of hours now and cannot wrap my head around what I have done wrong.

Edit: If I add the K switch it changes the error code to «‘/K’ is not recognized as an internal or external command, operable program or batch file.» So the only somewhat hopeful result my searches turned up doesn’t work.

Edit2: Turns out I am indeed an idiot and the /K switch had to be prepended to the copy.arguments value rather than the CmdString. I’ll leave this up incase anyone else makes a stupid mistake like I.

Thank you for reading.Sorry for wasting your time.

I’m translating a GNU Makefile into MSBuild XML.

I have the following sed command (part of a larger command):

... | sed 's/^Q(.*)/"&"/' | ...

When I execute just that sed portion in Cygwin, it «works» in the sense that it doesn’t error out.

However, after I’ve translated it to MSBuild XML — replaced the XML-sensitive symbols with ", &, ' — I get the error

sed: unsupported command ‘

I’m sure it’s not an issue with XML escaping issues; the Visual Studio build log says

Task Parameter:Command=»C:Program FilesGNU ARM EclipseBuild Tools2.8-201611221915binsed» ‘s/^Q(.*)/»&»/’ (TaskId:21)

sed: unsupported command ‘ (TaskId:21)

The command «»C:Program FilesGNU ARM EclipseBuild Tools2.8-201611221915binsed» ‘s/^Q(.*)/»&»/’ » exited with code 1. (TaskId:21)

The command was translated into the originally intended sed 's/^Q(.*)/"&"/'

However, there now appears to be an issue with cmd.exe.

With respect to cmd.exe, what part of that command doesn’t it like?
Is it the single/double quote? The ampersand?

I’m using the table from here.

  • Remove From My Forums
  • Question

  • Hi,

    This should be really easy… xcopy [source] [destination] but it isn’t working like that. My syntax is:

    xcopy STATA.LIC «C:Program Files (x86)Stata14» /R /H /Y

    Every tutorial and explanation I can find online details how to copy folders. I just want to copy one stupid file. The result is «Parse Error.» That means my syntax is wrong, according to online sources. I can’t see how to make it any more correct.
    I’ve tried placing quotes around STATA.LIC, using «COPY» instead of XCOPY, and no following switches at all. Even running from an administrative command prompt doesn’t work. I can copy the files without any errors in the GUI, but I need to do this
    via a script. STATA.LIC is in the source directory from where the command is being run.

    I’m baffled. Any help would be awesome.

    Thanks


    Jason

Answers

  • Parse errors are usually raised when you have unmatched quotes in your command. Since your quotes are matched, I suspect that you are not posting the command that generates the error message.

    Regardless of this, you should tell xcopy.exe that «Stata14» is a folder rather than a file. You do this with a trailing backslash:

    xcopy STATA.LIC «C:Program Files (x86)Stata14» /R /H /Y

    • Proposed as answer by

      Thursday, July 2, 2015 5:18 AM

    • Marked as answer by
      Kate LiMicrosoft employee
      Thursday, July 2, 2015 4:26 PM

  • I forewent the batch file and started entering the commands directly into an administrative command prompt. Old school… Still no luck.


    You now need to go back further with these commands:

    md  C:Source
    dir  > C:SourceTest.txt
    xcopy C:Source  C:Test1  /R /H /Y

    If this still does not work then you must tap F8 at boot time, then boot the machine into Repair Mode. Now open a Command Prompt and test the above commands again. Note that your drive letters might be different while in Repair Mode.

    • Marked as answer by
      Kate LiMicrosoft employee
      Thursday, July 2, 2015 4:25 PM

TTY Toolkit logo

TTY::Option

Gem Version
Actions CI
Build status
Maintainability
Coverage Status
Inline docs

Parser for command line arguments, keywords, options and environment variables

Features

  • Support for parsing of positional arguments, keyword arguments, flags, options and environment variables.
  • A convenient way to declare parsed parameters via DSL with a fallback to hash-like syntax.
  • Flexible parsing that doesn’t force any order for the parameters.
  • Handling of complex option and keyword argument inputs like lists and maps.
  • Many conversions types provided out of the box, from basic integer to more complex hash structures.
  • Automatic help generation that can be customised with usage helpers like banner, examples and more.
  • Parsing doesn’t raise errors by default and collects issues to allow for better user experience.
  • Ability to declare global options with inheritance that copies parameters to a child class.

Installation

Add this line to your application’s Gemfile:

And then execute:

Or install it yourself as:

Contents

  • 1. Usage
  • 2. API
    • 2.1 argument
    • 2.2 keyword
    • 2.3 option
    • 2.4 environment
    • 2.5 parameter settings
      • 2.5.1 arity
      • 2.5.2 convert
      • 2.5.3 default
      • 2.5.4 description
      • 2.5.5 hidden
      • 2.5.6 name
      • 2.5.7 optional
      • 2.5.8 permit
      • 2.5.9 required
      • 2.5.10 validate
    • 2.6 parse
      • 2.6.1 :raise_on_parse_error
      • 2.6.2 :check_invalid_params
    • 2.7 params
      • 2.7.1 errors
      • 2.7.2 remaining
      • 2.7.3 valid?
    • 2.8 usage
      • 2.8.1 header
      • 2.8.2 program
      • 2.8.3 command
      • 2.8.4 banner
      • 2.8.5 description
      • 2.8.6 example
      • 2.8.7 footer
    • 2.9 help
      • 2.9.1 sections
      • 2.9.2 :indent
      • 2.9.3 :order
      • 2.9.4 :param_display
      • 2.9.5 :width

1. Usage

To start parsing command line parameters include TTY::Option module.

Now, you’re ready to define parsed parameters like arguments, keywords, flags, options or environment variables.

For example, a quick demo to create a command that mixes all parameters usage:

class Command
  include TTY::Option

  usage do
    program "dock"

    command "run"

    desc "Run a command in a new container"

    example "Set working directory (-w)",
            "  $ dock run -w /path/to/dir/ ubuntu pwd"

    example <<~EOS
    Mount volume
      $ dock run -v `pwd`:`pwd` -w `pwd` ubuntu pwd
    EOS
  end

  argument :image do
    required
    desc "The name of the image to use"
  end

  argument :command do
    optional
    desc "The command to run inside the image"
  end

  keyword :restart do
    default "no"
    permit %w[no on-failure always unless-stopped]
    desc "Restart policy to apply when a container exits"
  end

  flag :help do
    short "-h"
    long "--help"
    desc "Print usage"
  end

  flag :detach do
    short "-d"
    long "--detach"
    desc "Run container in background and print container ID"
  end

  option :name do
    required
    long "--name string"
    desc "Assign a name to the container"
  end

  option :port do
    arity one_or_more
    short "-p"
    long "--publish list"
    convert :list
    desc "Publish a container's port(s) to the host"
  end

  def run
    if params[:help]
      print help
      exit
    else
      pp params.to_h
    end
  end
end

Then create a command instance:

And provided input from the command line:

restart=always -d -p 5000:3000 5001:8080 --name web ubuntu:16.4 bash

Start parsing from ARGV or provide a custom array of inputs:

cmd.parse
# or
cmd.parse(%w[restart=always -d -p 5000:3000 5001:8080 --name web ubuntu:16.4 bash])

And run the command to see the values:

cmd.run
# =>
# {:help=>false,
#  :detach=>true,
#  :port=>["5000:3000", "5001:8080"],
#  :name=>"web",
#  :restart=>"always",
#  :image=>"ubuntu:16.4",
#  :command=>"bash"}

The cmd object also has a direct access to all the parameters via the params:

cmd.params[:name]     # => "web"
cmd.params["command"] # => "bash

And when --help is found on the command line the run will print help:

To print help information to the terminal use help method:

This will result in the following output:

Usage: dock run [OPTIONS] IMAGE [COMMAND] [RESTART=RESTART]

Run a command in a new container

Arguments:
  IMAGE    The name of the image to use
  COMMAND  The command to run inside the image

Keywords:
  RESTART=RESTART  Restart policy to apply when a container exits (permitted:
                   no, on-failure, always, unless-stopped) (default "no")

Options:
  -d, --detach        Run container in background and print container ID
  -h, --help          Print usage
      --name string   Assign a name to the container
  -p, --publish list  Publish a container's port(s) to the host

Examples:
  Set working directory (-w)
    $ dock run -w /path/to/dir/ ubuntu pwd

  Mount volume
    $ dock run -v `pwd`:`pwd` -w `pwd` ubuntu pwd

2. API

2.1 argument

You can parse positional arguments with the argument method. To declare an argument you need to provide a name for the access key in the params like so:

Then parsing command line input:

Would result only in one argument parsed and the remaining ignored:

A more involved example to parse multiple positional arguments requires use of helper methods:

argument :foo do
  required                   # a default
  variable "foo(int)"        # name for the usage display
  arity one_or_more          # how many times to occur
  convert :int               # values converted to integer
  validate -> { |v| v < 14 } # validation rule
  desc "Some foo desc"       # description for the usage display
end

Parsing the previous input:

Would result in all values being collected and converted to integers:

params[:foo] # => [11,12,13]

The previous argument definition can also be written using hash syntax. This is especially useful if you want to specify arguments programmatically:

argument :foo,
  required: true,
  variable: "foo(int)",
  arity: "+",
  convert: :int,
  validate: -> { |v| v < 14 },
  desc: "Some foo desc"

To read more about available settings see parameter settings.

2.2 keyword

To parse keyword arguments use the keyword method. To declare a keyword argument you need to provide a name for the key in the params like so:

By default the keyword parameter name will be used as the keyword name on the command line:

Parsing the above would result in:

A more involved example to parse multiple keyword arguments requires use of helper methods:

keyword :foo do
  required                   # by default keywrod is not required
  arity one_or_more          # how many times to occur
  convert :int               # values converted to integer
  validate -> { |v| v < 14 } # validation rule
  desc "Some foo desc"       # description for the usage display
end

Then provided the following command line input:

The result would be:

params[:foo] # => [11,12,13]

You can also specify for the keyword argument to accept a list type:

keyword :foo do
  required                   # by default keyword is not required
  arity one_or_more          # how many times to occur
  convert :int_list          # input can be a list of integers
  validate -> { |v| v < 14 } # validation rule
  desc "Some foo desc"       # description for the usage display
end

Then command line input can contain a list as well:

Which will result in the same value:

params[:foo] # => [11,12,13]

A keyword definition can be also a hash. This is especially useful if you intend to specify keyword arguments programmatically:

keyword :foo,
  required: true,
  arity: :+,
  convert: :int_list,
  validate: -> { |v| v < 14 },
  desc: "Some foo desc"

To read more about available settings see parameter settings.

2.3 option

To parse options and flags use the option or flag methods.

To declare an option you need to provide a name for the key used to access value in the params:

By default the option parameter name will be used to generate a long option name:

Parsing the above will result in:

To specify a different name for the parsed option use the short and long helpers:

option :foo do
  short "-f"     # declares a short flag
  long  "--foo"  # declares a long flag
end

If you wish for an option to accept an argument, you need to provide an extra label.

For example, for both short and long flag to require argument do:

option :foo do
  short "-f"
  long  "--foo string"  # use any name after the flag name to specify required argument
  # or
  long  "--foo=string"  # you can also separate required argument with =
end

To make a long option with an optional argument do:

option :foo do
  long "--foo [string]" # use any name within square brackets to make argument optional
end

A more involved example that parses a list of integer may look like this:

option :foo do
  required                   # by default option is not required
  arity one_or_more          # how many times option can occur
  short "-f"                 # declares a short flag name
  long  "--foo list"         # declares a long flag with a required argument
  convert :int_list          # input can be a list of integers
  validate -> { |v| v < 14 } # validation rule
  desc "Some foo desc"       # description for the usage display
end

Given command line input:

The resulting value will be:

params[:foo] # => [10,11,12,13]

An option definition can be declared as a hash as well. This is especially useful if you intend to specify options programmatically:

option :foo,
  required: true,
  arity: :+,
  short: "-f",
  long: "--foo list",
  convert: :int_list,
  validate: -> { |v| v < 14 },
  desc: "Some foo desc"

To read more about available settings see parameter settings.

2.4 environment

To parse environment variables use environment or env methods.

By default, a parameter name will match a environment variable with the same name. For example, specifying a variable :foo:

And then given the following command line input:

The resulting parameter would be:

To change the variable name to something else use var or variable helper:

env :foo do
  var "FOO_ENV"
end

And then given a FOO_ENV=bar on the command line would result in:

A more involved example that parses a list of integer may look like this:

environment :foo do
  required                   # by default environment is not required
  arity one_or_more          # how many times env var can occur
  variable "FOO_ENV"         # the command line input name
  convert map_of(:int)       # input can be a map of integers
  validate -> { |v| v < 14 } # validation rule
  desc "Some foo desc"       # description for the usage display
end

Given command line input:

FOO_ENV=a:1&b:2 FOO_ENV=c=3 d=4

The resulting params would be:

params[:foo] # => {a:1,b:2,c:3,d:4}

To read more about available settings see parameter settings.

2.5 parameter settings

These settings are supported by all parameter types with the exception of short and long which are specific to options only.

2.5.1 arity

To describe how many times a given parameter may appear in the command line use the arity setting.

By default every parameter is assumed to appear only once. Any other occurrence will be disregarded and included in the remaining parameters list.

For example, to match argument exactly 2 times do:

argument :foo do
  arity 2
end

Then parsing from the command line:

Will give the following:

params[:foo] # => ["bar", "baz"]

For parameters that expect a value, specifying arity will collect all the values matching arity requirement. For example, matching keywords:

keyword :foo do
  arity 3
end

And then parsing the following:

Will produce:

params[:foo] # => ["1", "2", "3"]

To match any number of times use :any, :*, -1, any or zero_or_more:

argument :foo do
  arity zero_or_more
end

To match at at least one time use :+ or one_or_more:

option :foo do
  arity one_or_more
  short "-b"
  long "--bar string"
end

You can also specify upper boundary with at_least helper as well:

keyword :foo do
  arity at_least(3)
end

The help method will handle the arity for the display. Given the following argument definition:

argument :foo do
  arity one_or_more
end

The usage banner will display:

Usage: foobar FOO [FOO...]

2.5.2 convert

You can convert any parameter argument to another type using the convert method with a predefined symbol or class name. For example, to convert an argument to integer you can do:

argument :foo do
  convert :int
  # or
  convert Integer
end

The conversion types that are supported:

  • :boolean|:bool — e.g. ‘yes/1/y/t/’ becomes true, ‘no/0/n/f’ becomes false
  • :date — parses dates formats «28/03/2020», «March 28th 2020»
  • :float — e.g. -1 becomes -1.0
  • :int|:integer — e.g. +1 becomes 1
  • :path|:pathname — converts to Pathname object
  • :regexp — e.g. «foo|bar» becomes /foo|bar/
  • :uri — converts to URI object
  • :sym|:symbol — e.g. «foo» becomes :foo
  • :list|:array — e.g. ‘a,b,c’ becomes ["a", "b", "c"]
  • :map|:hash — e.g. ‘a:1 b:2 c:3’ becomes {a: "1", b: "2", c: "3"}

In addition you can specify a plural or append list to any base type:

  • :ints or :int_list — will convert to a list of integers
  • :floats or :float_list — will convert to a list of floats
  • :bools or :bool_list — will convert to a list of booleans, e.g. t,f,t becomes [true, false, true]

If like you can also use list_of helper and pass the type as a first argument.

Similarly, you can append map to any base type:

  • :int_map — will convert to a map of integers, e.g a:1 b:2 c:3 becomes {a: 1, b: 2, c: 3}
  • :bool_map — will convert to a map of booleans, e.g a:t b:f c:t becomes {a: true, b: false, c: true}

For convenience and readability you can also use map_of helper and pass the type as a first argument.

For example, to parse options with required list and map arguments:

option :foo do
  long "--foo map"
  convert :bools   # or `convert list_of(:bool)`
end

option :bar do
  long "--bar int map"
  convert :int_map   # or `conert map_of(:int)`
end

And then parsing the following:

--foo t,f,t --bar a:1 b:2 c:3

Will give the following:

params[:foo]
# => [true, false, true]
params[:bar]
# => {:a=>1, :b=>2, :c=>3}

You can also provide proc to define your own custom conversion:

option :bar do
  long "--bar string"
  convert ->(val) { val.upcase }
end

2.5.3 default

Any optional parameter such as options, flag, keyword or environment variable, can have a default value. This value can be specified with the default setting and will be used when the command-line input doesn’t match any parameter definitions.

For example, given the following option definition:

option :foo do
  long "--foo string"
  default "bar"
end

When no option --foo is parsed, then the params will be populated:

The default can also be specified with a proc object:

option :foo do
  long "--foo string"
  default -> { "bar" }
end

A parameter cannot be both required and have default value. Specifying both will raise ConfigurationError. For example, all positional arguments are required by default. If you want to have a default for a required argument make it optional:

argument :foo do
  optional
  default "bar"
  desc "Some description"
end

The default will be automatically displayed in the usage information:

Usage: foobar [OPTIONS] [FOO]

Arguments:
  FOO  Some description (default "bar")

2.5.4 desc(ription)

To provide a synopsis for a parameter use the description or shorter desc setting. This information is used by the help method to produce usage information:

option :foo do
  desc "Some description"
end

The above will result in:

Usage: foobar [OPTIONS]

Options:
  --foo  Some description

2.5.5 hidden

To hide a parameter from display in the usage information use the hidden setting:

argument :foo

argument :bar do
  hidden
end

The above will hide the :bar parameter from the usage:

2.5.6 name

By default the parameter key will be used to match command-line input arguments.

This means that a key :foo_bar will match "foo-bar" parameter name. For example, given a keyword:

And then command-line input:

The parsed result will be:

params[:foo_bar] # => "baz"

To change the parameter name to a custom one, use the name setting:

keyword :foo_bar do
  name "fum"
end

Then parsing:

Will result in:

For environment variables use the upper case when changing name:

env :foo do
  name "FOO_VAR"
end

2.5.7 optional

Apart from the positional argument, all other parameters are optional. To mark an argument as optional use similar naming optional setting:

argument :foo do
  desc "Foo arg description"
end

argument :bar do
  optional
  desc "Bar arg description"
end

The optional argument will be surrounded by brackets in the usage display:

Usage: foobar [OPTIONS] FOO [BAR]

Arguments:
  FOO  Foo arg description
  BAR  Bar arg description

2.5.8 permit

The permit setting allows you to restrict an input to a set of possible values.

For example, let’s restrict option to only "bar" and "baz" strings:

option :foo do
  long "--foo string"
  permit ["bar", "baz"]
end

And then parsing

Will populate parameters value:

Attempting to parse not permitted value:

Will internally produce a TTY::Option::UnpermittedArgument error and make the params invalid.

Permitted values are checked after applying conversion. Because of this, you need to provide the expected type for the permit setting:

option :foo do
  long "--foo int"
  confert :int
  permit [11, 12, 13]
end

Then parsing an unpermitted value:

Will invalidate params and collect the TTY::Option::UnpermittedArgument error.

The permitted values are automatically appended to the parameter synopsis when displayed in the usage information. For example, given an option:

option :foo do
  short "-f"
  long "--foo string"
  permit %w[a b c d]
  desc "Some description"
end

Then the usage information for the option would be:

Usage: foobar [OPTIONS]

Options:
  -f, --foo string  Some description (permitted: a,b,c,d)

2.5.9 required

Only arguments are required. Any other parameters like options, keywords and environment variables are optional. To force parameter presence in input use required setting.

keyword :foo do
  required
  desc "Foo keyword description"
end

keyword :bar do
  desc "Bar keyword description"
end

Because foo keyword is required it won’t have brackets around the parameter in the usage display:

Usage: foobar FOO=FOO [BAR=BAR]

Keywords:
  FOO=FOO  Foo keyword description
  BAR=BAR  Bar keyword description

Note: Using required options is rather discouraged as these are typically expected to be optional.

2.5.10 validate

Use the validate setting if you wish to ensure only inputs matching filter criteria are allowed.

You can use a string or regular expression to describe your validation rule:

option :foo do
  long "--foo VAL"
  validate "d+"
end

Then parsing:

Will internally cause an exception TTY::Option::InvalidArgument that will make params invalid.

You can also express a validation rule with a proc object:

keyword :foo do
  arity one_or_more
  convert :int
  validate ->(val) { val < 12 }
end

Then parsing:

Will similarly collect the TTY::Option::InvalidArgument error and render params invalid.

2.6 parse

After all parameters are defined, use the parse to process command line inputs.

By default the parse method takes the input from the ARGV and the ENV variables.

Alternatively, you can call parse with custom inputs. This is especially useful for testing your commands.

Given parameter definitions:

argument :foo

flag :bar

keyword :baz

env :qux

Then parsing the following inputs:

parse(%w[12 --bar baz=a QUX=b])

Would populate parameters:

params[:foo] # => "12"
params[:bar] # => true
params[:baz] # => "a"
params[:qux] # => "b"

The parsing is flexible and doesn’t force any order for the parameters. Options can be inserted anywhere between positional or keyword arguments.

It handles parsing of compacted shorthand options that start with a single dash. These need to be boolean options bar the last one that can accept argument. All these are valid:

-f
-fbq
-fbqs 12  # mixed with an argument

Parameter parsing stops after the -- terminator is found. The leftover inputs are collected and accessible via the remaining method.

2.6.1 :raise_on_parse_error

By default no parse errors are raised. Why? Users do not appreciate Ruby errors in their terminal output. Instead, parsing errors are made accessible on the params object with the errors method.

However, if you prefer to handle parsing errors yourself, you can do so with :raise_on_parse_error keyword:

parse(raise_on_parse_error: true)

Then in your code you may want to surround your parse call with a rescue clause:

begin
  parse(raise_on_parse_error: true)
rescue TTY::Option::ParseError => err
  # do something here
end

2.6.2 :check_invalid_params

Users can provide any input, including parameters you didn’t expect and define.

By default, when unknown parameter is found in the input, an TTY::Option::InvalidParameter error will be raised internally and collected in the errors list.

If, on the other hand, you want to ignore unknown parameters and instead leave them alone during the parsing use the :check_invalid_params option like so:

parse(check_invalid_params: false)

This way all the unrecognized parameters will be collected into a remaining list accessible on the params instance.

2.7 params

Once all parameters are defined, they are accessible via the params instance method.

The params behaves like a hash with an indifferent access. It doesn’t distinguish between arguments, keywords or options. Each parameter needs to have a unique identifier.

For example, given a command with all parameter definitions:

class Command
  include TTY::Option

  argument :foo

  keyword :bar

  option :baz

  env :qux

  def run
    print params[:foo]
    print params["bar"]
    print params["baz"]
    print params[:qux]
  end
end

Then parsing the command:

cmd = Command.new
cmd.parse

With the command-line input:

And running the command:

Will output:

2.7.1 errors

Only configuration errors are raised. The parsing errors are not raised by default. Instead any parse error is made available via the errors method on the params object:

params.errors
# => AggregateErors

The returned AggregateErrors object is an Enumerable that allows you to iterate over all of the errors.

It has also a convenience methods like:

  • messages — access all error messages as an array
  • summary — a string of nicely formatted error messages ready to display in terminal

For example, let’s say we have an argument definition that requires at least 2 occurrences on the command line:

argument :foo do
  arity at_least(2)
end

And only one argument is provided in the input. Then output summary:

puts params.errors.summary

Would result in the following being printed:

Error: argument 'foo' should appear at least 2 times but appeared 1 time

Let’s change the previous example and add conversion to the mix:

argument :foo do
  arity at_least(2)
  convert :int
end

And provided only one argument string «zzz», the summary would be:

Errors:
  1) Argument 'foo' should appear at least 2 times but appeared 1 time
  2) Cannot convert value of `zzz` into 'int' type for 'foo' argument

If, on the other hand, you prefer to raise errors, you can do so using the :raise_on_parse_error keyword:

parse(raise_on_parse_error: true)

This way any attempt at parsing invalid input will raise to the terminal.

2.7.2 remaining

Users can provide any input, including parameters you didn’t expect and define.

By default, when unknown parameter is found in the input, an TTY::Option::InvalidParameter error will be raised internally and collected in the errors list.

If, on the other hand, you want to ignore unknown parameters and instead leave them alone during the parsing use the :check_invalid_params option like so:

parse(check_invalid_params: true)

This way all the unrecognized parameters will be collected into a list. You can access them on the params instance with the remaining method.

For example, let’s assume that user provided --unknown option that we didn’t expect. Inspecting the remaining parameters, we would get:

params.remaining # => ["--unknown"]

Any parameters after the -- terminator will be left alone during the parsing process and collected into the remaining list. This is useful in situations when you want to pass parameters over to another command-line applications.

2.7.3 valid?

Once parsing of the command-line input is done, you can check if all the conditions defined by the parameters are met with the valid? method.

You can use this to decide how to deal with parsing errors and what exit status to use.

For example, you can decide to implement a command method like this:

if params.valid?
  # ... process params
else
  puts params.errors.summary
  exit
end

You can combine errors reporting with existing with the tty-exit module.

The TTY::Exit module exposes the exit_with method and can be used like this:

class Command
  include TTY::Exit
  include TTY::Option

  def run
    if params.valid?
      # ... process params
    else
      exit_with(:usage_error, params.errors.summary)
    end
  end
end

2.8 usage

The usage and its helper methods allow you to configure the help display to your liking. The header, desc(ription), example and footer can be called many times. Each new call will create a new paragraph. If you wish to insert multiple lines inside a given paragraph separate arguments with a comma.

2.8.1 header

To provide information above the banner explaining how to execute a program, use the header helper.

usage do
  header "A command-line interface for foo service"
end

Further, you can add more paragraphs as comma-separated arguments to header with an empty string to represent a new line:

usage do
  header "A command-line interface for foo service",
         "",
         "Access and retrieve data from foo service"
end

Alternatively, you can add paragraphs calling header multiple times:

usage do
  header "A command-line interface for foo service"

  header "Access and retrieve data from foo service"
end

2.8.2 program

By default the program name is inferred for you from the executable file name.

You can override the default name using the program helper.

usage do
  program "custom-name"
end

Then the program name will be used in the banner:

2.8.3 command

By default the command name is inferred from the class name.

For example, based on the following:

class NetworkCreate
  include TTY::Option
end

The command name will become network-create. To change this use the command and commands helpers:

class NetworkCreate
  include TTY::Option

  usage do
    commands "network", "create"
  end
end

This will result in the following usage information:

Usage: program network create

If you don’t wish to infer the command name use the no_command method:

2.8.4 banner

The usage information of how to use a program is displayed right after header. If no header is specified, it will be displayed first.

This information is handled by the banner helper. By default, it will use the parameter definitions to generate usage information.

For example, given the following declarations:

usage do
  program :foo

  command :bar
end

argument :baz

keyword :qux do
  convert :uri
end

option :fum

The generated usage information will be:

Usage: foo bar [OPTIONS] BAZ [QUX=URI]

If you want to configure how arguments are displayed specify 2.8.2 :param_display setting.

You can also change completely how to the banner is displayed:

usage do
  program "foo"

  banner "Usage: #{program} BAR BAZ"
end

2.8.5 desc(ription)

The description is placed between usage information and the parameters and given with desc or description helpers.

The desc helper accepts multiple strings that will be displayed on separate lines.

usage do
  desc "Some description", "on multiline"
end

This will result in the following help output:

Some description
on multiline

The desc helper can be called multiple times to build an examples section:

usage do
  desc "Some description", "on multiline"

  desc <<~EOS
  Another description
  on multiline
  EOS
end

2.8.6 example(s)

To add usage examples section to the help information use the example or examples methods.

The example helper accepts multiple strings that will be displayed on separate lines. For instance, the following class will add a single example:

usage do
  example "Some example how to use foo",
          " $ foo bar"
end

This will result in the following help output:

Examples:
  Some example how to use foo
    $ foo bar

The example helper can be called multiple times to build an examples section:

usage do
  example "Some example how to use foo",
          " $ foo bar"

  example <<~EOS
  Another example how to use foo"
    $ foo baz
  EOS
end

The usage help will contain the following:

Examples:
  Some example how to use foo
    $ foo bar

  Another example how to use foo
    $ foo baz

2.8.7 footer

To provide information after all information in the usage help, use the footer helper.

usage do
  footer "Run a command followed by --help to see more info"
end

Further, you can add more paragraphs as comma-separated arguments to footer with an empty string to represent a new line:

usage do
  footer "Run a command followed by --help to see more info",
         "",
         "Options marked with (...) can be given more than once"
end

Alternatively, you can add paragraphs calling footer multiple times:

usage do
  footer "Run a command followed by --help to see more info"

  footer "Options marked with (...) can be given more than once"
end

2.9 help

With the help instance method you can generate usage information from the defined parameters and the usage. The usage describes how to add different sections to the help display.

Let’s assume you have the following command with a run method that prints help:

class Command
  include TTY::Option

  usage do
    program "foobar",
    header  "foobar CLI"
    desc    "Some foobar description"
    example "Some example"
    footer  "Run --help to see more info"
  end

  argument :bar, desc: "Some argument description"
  keyword :baz, desc: "Some keyword description"
  env :fum, desc: "Some env description"

  flag :help do
    short "-h"
    long  "--help"
    desc "Print usage"
  end

  def run
    if params[:help]
      print help
      exit
    end
  end
end

Running the command with --help flag:

cmd = Command.new
cmd.parse(%w[--help])
cmd.run

Will produce:

foobar CLI

Usage: foobar [OPTIONS] [ENVIRONMENT] BAR [BAZ=BAZ]

Some foobar description

Arguments:
  BAR  Some argument description

Keywords:
  BAZ=BAZ  Some keyword description

Options:
  -h, --help Print usage

Envrionment:
  FUM  Some env description

Examples:
  Some example

Run --help to see more info

2.9.1 sections

It is possible to change the usage content by passing a block to help. The help method yields an object that contains all the sections and provides a hash-like access to each of its sections.

The following are the names for all supported sections:

  • :header
  • :banner
  • :description
  • :arguments
  • :keywords
  • :options
  • :environments
  • :exmaples
  • :footer

You can use add_before, add_after, delete and replace to modify currently existing sections or add new ones.

For example, to remove a header section do:

help do |sections|
  sections.delete :header
end

To insert a new section after :arguments called :commands do:

help do |sections|
  sections.add_after :arguments, :commands,
                     "nCommands:n  create  A command description"
end

To replace a section’s content use replace:

help do |sections|
  sections.replace :footer, "nGoodbye"
end

2.9.2 :indent

By default has not indentation for any of the sections bar parameters.

To change the indentation for the entire usage information use :indent keyword:

2.9.3 :order

All parameters are alphabetically ordered in their respective sections. To change this default behaviour use the :order keyword when invoking help.

The :order expects a Proc object. For example, to remove any ordering and preserve the parameter declaration order do:

help(order: ->(params) { params })

2.9.4 :param_display

By default banner positional and keyword arguments are displayed with all letters uppercased.

For example, given the following parameter declarations:

program "run"

argument :foo

keyword :bar do
  required
  convert :uri
end

option :baz

The banner output would be as follows:

Usage: run [OPTIONS] FOO BAR=URI

To change the banner parameter display use :param_display keyword.

For example, to lowercase and surround your parameters with < > brackets do:

help(param_display: ->(str) { "<#{str.downcase}>" })

This will produce the following output:

Usage: run [<options>] <foo> <bar>=<uri>

2.9.5 :width

By default the help information is wrapped at 80 columns. If this is not what you want you can change it with :width keyword.

For example, to change the help to always take up all the terminal columns consider using tty-screen:

help(width: TTY::Screen.width)

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/piotrmurach/tty-option. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the TTY::Option project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

Copyright

Copyright (c) 2020 Piotr Murach. See LICENSE for further details.

Добавлено через 7 минут
Информация от Aacini (с машинным переводом).

В этом ответе описываются значения ERRORLEVEL, возвращаемые всеми внутренними командами cmd.exe; они сгруппированы по способу изменения стоимости и представлены в виде кратких справочных таблиц. Я просмотрел другие подобные таблицы, чтобы собрать этот, но заполнил недостающие значения с помощью тестов, выполненных на компьютере под управлением Windows 8.1. Я приложил все усилия, чтобы эти таблицы были полными и точными, но я не тестировал каждый из приведенных здесь значений, поэтому это могут быть тонкие несоответствия.

Таблица 1 — Команды, которые не изменяют предыдущее значение ERRORLEVEL

Код

BREAK
ECHO
ENDLOCAL
FOR      Not change the ERRORLEVEL by itself. See "Exit Code" below.
IF       Not change the ERRORLEVEL by itself.
PAUSE
RD       Not change the ERRORLEVEL on errors, but the "Exit Code". See below.
REM
RMDIR    Same as RD.
SET      Plain SET command (no arguments). See "Table 3" below.
TITLE

Таблица 2 — Команды, которые устанавливают ERRORLEVEL в 0 или 1 в зависимости от результата

Код

Command │ Set ERRORLEVEL = 0 when       │ Set ERRORLEVEL = 1 when
────────┼───────────────────────────────┼─────────────────────────────────────────────────────────────
CD      │Current directory was changed. │Directory not exists or is not accessible.
CHDIR   │Same as CD.                    │
COLOR   │Color was changed.             │Background and foreground colors are the same.
COPY    │File(s) was processed.         │File not found or bad parameters given.
DATE    │Date was changed or not given. │User has no admin privileges.
DEL     │Almost always, excepting when: │Bad or no parameters given.
DIR     │Same as COPY.                  │
ERASE   │Same as DEL.                   │
MD      │Directory was created.         │Directory could not be created.
MKDIR   │Same as MD.                    │
MKLINK  │Link was created.              │Link could not be created or bad parameters given.
MOVE    │File(s) was moved/renamed.     │File not found, could not be moved/renamed or bad parameters.
PUSHD   │Same as CD.                    │+ Bad switch given.
REN     │Same as MOVE.                  │
RENAME  │Same as MOVE.                  │
SETLOCAL│New environment was created.   │Bad parameters given.
TIME    │Time was changed or not given. │User has no admin privileges.
TYPE    │Same as COPY.                  │
VERIFY  │Right or no parameters given.  │Bad parameters given.
VOL     │Volume label was displayed.    │Drive not found or bad parameters given.

Таблица 3 — Команды, которые устанавливают ошибку ERRORLEVEL; в противном случае не меняйте его

Код

Command      │E│ Set ERRORLEVEL to = when
─────────────┼─┼────────────────────────────────────────────────────────────────────────
ASSOC        │*│1 = Extension associations could not be changed.
CLS          │ │1 = Bad switch given.
DPATH        │*│1 = Data path could not be established.
FTYPE        │*│1 = File type associations could not be changed.
GOTO label   │ │1 = Label not exist *in a subroutine* (equivalent to: EXIT /B 1).
KEYS         │ │1 = Bad switch given.
PATH         │*│1 = Path could not be changed.
POPD         │ │1 = Bad switch given.
PROMPT       |*│1 = Prompt could not be changed.
SET var      │*│1 = No variable with such name exists.
SET var=value│*│1 = Variable name start with "/" not enclosed in quotes.
SET /P       │*│1 = Read an empty line or at end of file.
SET /A       │*│1073750988 = Unbalanced parentheses, 1073750989 = Missing operand, 
             │ │1073750990 = Syntax error, 1073750991 = Invalid number,
             │ │1073750992 = Number larger than 32-bits, 1073750993 = Division by zero.
SHIFT        │ │1 = Bad switch given.

В столбце «E» в таблице 3 указаны команды, которые изменяют свое поведение соответственно статусу «Расширения», как описано в соответствующей документации. Когда расширения включены (по умолчанию), и эти команды помещаются в файл с расширением .CMD вместо .BAT one, эти команды устанавливают SETERRORLEVEL = 0, когда они заканчиваются без ошибок, то есть когда условия, описанные в таблице 3 нет.

Таблица 4 — Особые случаи

Код

CALL Table1     │If the called command is anyone of Table 1 (excepting FOR and IF): set ERRORLEVEL = 0.
CALL subroutine │If the subroutine is called, not change prior ERRORLEVEL value;
                │otherwise (subroutine not exists): set ERRORLEVEL = 1.
EXIT /B, EXIT   │Not change prior ERRORLEVEL value.
EXIT /B number  │Set ERRORLEVEL to given number.
EXIT number     │Ends cmd.exe and set its returning ERRORLEVEL value to given number.
START command   │If command is started, not change ERRORLEVEL; otherwise, set ERRORLEVEL = 9059.
START /WAIT bat |When the started Batch file end, set ERRORLEVEL = value from 'EXIT number' commmand.
notExist        │If a non-existent command is entered for execution, set ERRORLEVEL = 9009.
VER             │Set ERRORLEVEL = 0 almost always. If /? parameter is given, not change ERRORLEVEL.

Управление кодом выхода
Существует два способа проверить значение ERRORLEVEL: через команду IF ERRORLEVEL / IF %ERRORLEVEL%или с помощью конструкции command && thenCmd when ERRORLEVEL is 0 || elseCmd when ERRORLEVEL is not 0. Однако некоторые определенные команды и ошибки перенаправления возвращают значение, которое работает только во втором случае и не отражается в ERRORLEVEL; мы можем назвать это «Код выхода». Если этот код выхода не равен нулю, его можно передать на ERRORLEVEL, выполнив любую команду в таблице 1 в части elseCmd. Вы можете прочитать дополнительную информацию по этому вопросу в этом сообщении.

Таблица 5 — Команды или функции, которые устанавливают код выхода

Код

Feature      │ Set Exit Code to = when
─────────────┼─────────────────────────────────────────────────────────────────────────
command      │1 = Command not exist (when ERRORLEVEL = 9009).
redirection  │1 = File not exists in "<", path not exists or access denied in ">" ">>".
drive:       |1 = Drive unit not exists.
POPD         |1 = No matching PUSHD was previously executed.
RD           │1 = Bad switch given, 2 = Directory not found, 5 = Access denied,
             │32 = Directory in use, 145 = Directory not empty.
FOR /F       │1 = No data was processed.

Например, чтобы проверить, произошла ли ошибка перенаправления, используйте это:

Код

command > C:Paththatdoesnotexistfile.txt || rem
if errorlevel 1 echo Previous redirection failed

В этом примере команда rem используется для копирования кода выхода в ERRORLEVEL, но может использоваться любая другая внутренняя команда, которая сохраняет ERRORLEVEL (кроме FOR и IF).
Проверить, существует ли накопитель:

Код

U: || rem
if errorlevel 1 echo Previous set current drive to U: unit failed

Другие примеры:

Код

rd c:Somedirectory 2> NUL || rem
if %errorlevel% equ 0 (
   echo Directory deleted
) else if %errorlevel% equ 2 (
   echo Directory not found
) else if %errorlevel% equ 5 (
   echo Can not access the directory, check rights
) else if %errorlevel% equ 32 (
   echo Can not delete current directory
) else if %errorlevel% equ 145 (
   echo Directory is not empty, use /S switch
)
(for /F "options" %%a in (input.txt) do echo %%a) || rem
if errorlevel 1 echo Previous FOR didn't processed any value



2



When invoking a command from a command window, tokenization of the command line arguments is not done by cmd.exe (a.k.a. «the shell»). Most often the tokenization is done by the newly formed processes’ C/C++ runtime, but this is not necessarily so — for example, if the new process was not written in C/C++, or if the new process chooses to ignore argv and process the raw commandline for itself (e.g. with GetCommandLine()). At the OS level, Windows passes command lines untokenized as a single string to new processes. This is in contrast to most *nix shells, where the shell tokenizes arguments in a consistent, predictable way before passing them to the newly formed process. All this means that you may experience wildly divergent argument tokenization behavior across different programs on Windows, as individual programs often take argument tokenization into their own hands.

If it sounds like anarchy, it kind of is. However, since a large number of Windows programs do utilize the Microsoft C/C++ runtime’s argv, it may be generally useful to understand how the MSVCRT tokenizes arguments. Here is an excerpt:

  • Arguments are delimited by white space, which is either a space or a tab.
  • A string surrounded by double quotation marks is interpreted as a single argument, regardless of white space contained within. A quoted string can be embedded in an argument. Note that the caret (^) is not recognized as an escape character or delimiter.
  • A double quotation mark preceded by a backslash, «, is interpreted as a literal double quotation mark («).
  • Backslashes are interpreted literally, unless they immediately precede a double quotation mark.
  • If an even number of backslashes is followed by a double quotation mark, then one backslash () is placed in the argv array for every pair of backslashes (), and the double quotation mark («) is interpreted as a string delimiter.
  • If an odd number of backslashes is followed by a double quotation mark, then one backslash () is placed in the argv array for every pair of backslashes () and the double quotation mark is interpreted as an escape sequence by the remaining backslash, causing a literal double quotation mark («) to be placed in argv.

The Microsoft «batch language» (.bat) is no exception to this anarchic environment, and it has developed its own unique rules for tokenization and escaping. It also looks like cmd.exe’s command prompt does do some preprocessing of the command line argument (mostly for variable substitution and escaping) before passing the argument off to the newly executing process. You can read more about the low-level details of the batch language and cmd escaping in the excellent answers by jeb and dbenham on this page.


Let’s build a simple command line utility in C and see what it says about your test cases:

int main(int argc, char* argv[]) {
    int i;
    for (i = 0; i < argc; i++) {
        printf("argv[%d][%s]n", i, argv[i]);
    }
    return 0;
}

(Notes: argv[0] is always the name of the executable, and is omitted below for brevity. Tested on Windows XP SP3. Compiled with Visual Studio 2005.)

> test.exe "a ""b"" c"
argv[1][a "b" c]

> test.exe """a b c"""
argv[1]["a b c"]

> test.exe "a"" b c
argv[1][a" b c]

And a few of my own tests:

> test.exe a "b" c
argv[1][a]
argv[2][b]
argv[3][c]

> test.exe a "b c" "d e
argv[1][a]
argv[2][b c]
argv[3][d e]

> test.exe a "b" c
argv[1][a]
argv[2]["b"]
argv[3][c]

Percent Expansion Rules

Here is an expanded explanation of Phase 1 in jeb’s answer (valid for both batch mode and command line mode).

Phase 1) Percent Expansion
Starting from left, scan each character for % or <LF>. If found then

  • 1.05 (truncate line at <LF>)
  • If the character is <LF> then
    • Drop (ignore) the remainder of the line from the <LF> onward
    • Goto Phase 2.0
  • Else the character must be %, so proceed to 1.1
  • 1.1 (escape %) skipped if command line mode
  • If batch mode and followed by another % then
    Replace %% with single % and continue scan
  • 1.2 (expand argument) skipped if command line mode
  • Else if batch mode then
    • If followed by * and command extensions are enabled then
      Replace %* with the text of all command line arguments (Replace with nothing if there are no arguments) and continue scan.
    • Else if followed by <digit> then
      Replace %<digit> with argument value (replace with nothing if undefined) and continue scan.
    • Else if followed by ~ and command extensions are enabled then
      • If followed by optional valid list of argument modifiers followed by required <digit> then
        Replace %~[modifiers]<digit> with modified argument value (replace with nothing if not defined or if specified $PATH: modifier is not defined) and continue scan.
        Note: modifiers are case insensitive and can appear multiple times in any order, except $PATH: modifier can only appear once and must be the last modifier before the <digit>
      • Else invalid modified argument syntax raises fatal error: All parsed commands are aborted, and batch processing aborts if in batch mode!
  • 1.3 (expand variable)
  • Else if command extensions are disabled then
    Look at next string of characters, breaking before % or end of buffer, and call them VAR (may be an empty list)

    • If next character is % then
      • If VAR is defined then
        Replace %VAR% with value of VAR and continue scan
      • Else if batch mode then
        Remove %VAR% and continue scan
      • Else goto 1.4
    • Else goto 1.4
  • Else if command extensions are enabled then
    Look at next string of characters, breaking before % : or end of buffer, and call them VAR (may be an empty list). If VAR breaks before : and the subsequent character is % then include : as the last character in VAR and break before %.

    • If next character is % then
      • If VAR is defined then
        Replace %VAR% with value of VAR and continue scan
      • Else if batch mode then
        Remove %VAR% and continue scan
      • Else goto 1.4
    • Else if next character is : then
      • If VAR is undefined then
        • If batch mode then
          Remove %VAR: and continue scan.
        • Else goto 1.4
      • Else if next character is ~ then
        • If next string of characters matches pattern of [integer][,[integer]]% then
          Replace %VAR:~[integer][,[integer]]% with substring of value of VAR (possibly resulting in empty string) and continue scan.
        • Else goto 1.4
      • Else if followed by = or *= then
        Invalid variable search and replace syntax raises fatal error: All parsed commands are aborted, and batch processing aborts if in batch mode!
      • Else if next string of characters matches pattern of [*]search=[replace]%, where search may include any set of characters except =, and replace may include any set of characters except %, then
        Replace %VAR:[*]search=[replace]% with value of VAR after performing search and replace (possibly resulting in empty string) and continue scan
      • Else goto 1.4
  • 1.4 (strip %)
    • Else If batch mode then
      Remove % and continue scan starting with the next character after the %
    • Else preserve the leading % and continue scan starting with the next character after the preserved leading %

The above helps explain why this batch

@echo off
setlocal enableDelayedExpansion
set "1var=varA"
set "~f1var=varB"
call :test "arg1"
exit /b  
::
:test "arg1"
echo %%1var%% = %1var%
echo ^^^!1var^^^! = !1var!
echo --------
echo %%~f1var%% = %~f1var%
echo ^^^!~f1var^^^! = !~f1var!
exit /b

Gives these results:

%1var% = "arg1"var
!1var! = varA
--------
%~f1var% = P:arg1var
!~f1var! = varB

Note 1 — Phase 1 occurs prior to the recognition of REM statements. This is very important because it means even a remark can generate a fatal error if it has invalid argument expansion syntax or invalid variable search and replace syntax!

@echo off
rem %~x This generates a fatal argument expansion error
echo this line is never reached

Note 2 — Another interesting consequence of the % parsing rules: Variables containing : in the name can be defined, but they cannot be expanded unless command extensions are disabled. There is one exception — a variable name containing a single colon at the end can be expanded while command extensions are enabled. However, you cannot perform substring or search and replace operations on variable names ending with a colon. The batch file below (courtesy of jeb) demonstrates this behavior

@echo off
setlocal
set var=content
set var:=Special
set var::=double colon
set var:~0,2=tricky
set var::~0,2=unfortunate
echo %var%
echo %var:%
echo %var::%
echo %var:~0,2%
echo %var::~0,2%
echo Now with DisableExtensions
setlocal DisableExtensions
echo %var%
echo %var:%
echo %var::%
echo %var:~0,2%
echo %var::~0,2%

Note 3 — An interesting outcome of the order of the parsing rules that jeb lays out in his post: When performing find and replace with delayed expansion, special characters in both the find and replace terms must be escaped or quoted. But the situation is different for percent expansion — the find term must not be escaped (though it can be quoted). The percent replace string may or may not require escape or quote, depending on your intent.

@echo off
setlocal enableDelayedExpansion
set "var=this & that"
echo %var:&=and%
echo "%var:&=and%"
echo !var:^&=and!
echo "!var:&=and!"

Delayed Expansion Rules

Here is an expanded, and more accurate explanation of Phase 5 in jeb’s answer (valid for both batch mode and command line mode)

Phase 5) Delayed Expansion

This phase is skipped if any of the following conditions apply:

  • Delayed expansion is disabled.
  • The command is within a parenthesized block on either side of a pipe.
  • The incoming command token is a «naked» batch script, meaning it is not associated with CALL, parenthesized block, any form of command concatenation (&, && or ||), or a pipe |.

The delayed expansion process is applied to tokens independently. A command may have multiple tokens:

  • The command token. For most commands the command name itself is a token. But a few commands have specialized regions that are considered a TOKEN for Phase 5.
    • for ... in(TOKEN) do
    • if defined TOKEN
    • if exists TOKEN
    • if errorlevel TOKEN
    • if cmdextversion TOKEN
    • if TOKEN comparison TOKEN, where comparison is one of ==, equ, neq, lss, leq, gtr, or geq
  • The arguments token
  • The destination token of redirection (one per redirection)

No change is made to tokens that do not contain !.

For each token that does contain at least one !, scan each character from left to right for ^ or !, and if found, then

  • 5.1 (caret escape) Needed for ! or ^ literals
  • If character is a caret ^ then
    • Remove the ^
    • Scan the next character and preserve it as a literal
    • Continue the scan
  • 5.2 (expand variable)
  • If character is !, then
    • If command extensions are disabled then
      Look at next string of characters, breaking before ! or <LF>, and call them VAR (may be an empty list)

      • If next character is ! then
        • If VAR is defined, then
          Replace !VAR! with value of VAR and continue scan
        • Else if batch mode then
          Remove !VAR! and continue scan
        • Else goto 5.2.1
      • Else goto 5.2.1
    • Else if command extensions are enabled then
      Look at next string of characters, breaking before !, :, or <LF>, and call them VAR (may be an empty list). If VAR breaks before : and the subsequent character is ! then include : as the last character in VAR and break before !

      • If next character is ! then
        • If VAR exists, then
          Replace !VAR! with value of VAR and continue scan
        • Else if batch mode then
          Remove !VAR! and continue scan
        • Else goto 5.2.1
      • Else if next character is : then
        • If VAR is undefined then
          • If batch mode then
            Remove !VAR: and continue scan
          • Else goto 5.2.1
        • Else if next character is ~ then
          • If next string of characters matches pattern of [integer][,[integer]]! then Replace !VAR:~[integer][,[integer]]! with substring of value of VAR (possibly resulting in empty string) and continue scan.
          • Else goto 5.2.1
        • Else if next string of characters matches pattern of [*]search=[replace]!, where search may include any set of characters except =, and replace may include any set of characters except !, then
          Replace !VAR:[*]search=[replace]! with value of VAR after performing search and replace (possibly resulting in an empty string) and continue scan
        • Else goto 5.2.1
      • Else goto 5.2.1
    • 5.2.1
      • If batch mode then remove the leading !
        Else preserve the leading !
      • Continue the scan starting with the next character after the preserved leading !

As pointed out, commands are passed the entire argument string in μSoft land, and it is up to them to parse this into separate arguments for their own use. There is no consistencty in this between different programs, and therefore there is no one set of rules to describe this process. You really need to check each corner case for whatever C library your program uses.

As far as the system .bat files go, here is that test:

c> type args.cmd
@echo off
echo cmdcmdline:[%cmdcmdline%]
echo 0:[%0]
echo *:[%*]
set allargs=%*
if not defined allargs goto :eof
setlocal
@rem Wot about a nice for loop?
@rem Then we are in the land of delayedexpansion, !n!, call, etc.
@rem Plays havoc with args like %t%, a"b etc. ugh!
set n=1
:loop
    echo %n%:[%1]
    set /a n+=1
    shift
    set param=%1
    if defined param goto :loop
endlocal

Now we can run some tests. See if you can figure out just what μSoft are trying to do:

C>args a b c
cmdcmdline:[cmd.exe ]
0:[args]
*:[a b c]
1:[a]
2:[b]
3:[c]

Fine so far. (I’ll leave out the uninteresting %cmdcmdline% and %0 from now on.)

C>args *.*
*:[*.*]
1:[*.*]

No filename expansion.

C>args "a b" c
*:["a b" c]
1:["a b"]
2:[c]

No quote stripping, though quotes do prevent argument splitting.

c>args ""a b" c
*:[""a b" c]
1:[""a]
2:[b" c]

Consecutive double quotes causes them to lose any special parsing abilities they may have had. @Beniot’s example:

C>args "a """ b "" c"""
*:["a """ b "" c"""]
1:["a """]
2:[b]
3:[""]
4:[c"""]

Quiz: How do you pass the value of any environment var as a single argument (i.e., as %1) to a bat file?

c>set t=a "b c
c>set t
t=a "b c
c>args %t%
1:[a]
2:["b c]
c>args "%t%"
1:["a "b]
2:[c"]
c>Aaaaaargh!

Sane parsing seems forever broken.

For your entertainment, try adding miscellaneous ^, , ', & (&c.) characters to these examples.

We performed experiments to investigate the grammar of batch scripts. We also investigated differences between batch and command line mode.

Batch Line Parser:

Here is a brief overview of phases in the batch file line parser:

Phase 0) Read Line:

Phase 1) Percent Expansion:

Phase 2) Process special characters, tokenize, and build a cached command block: This is a complex process that is affected by things such as quotes, special characters, token delimiters, and caret escapes.

Phase 3) Echo the parsed command(s) Only if the command block did not begin with @, and ECHO was ON at the start of the preceding step.

Phase 4) FOR %X variable expansion: Only if a FOR command is active and the commands after DO are being processed.

Phase 5) Delayed Expansion: Only if delayed expansion is enabled

Phase 5.3) Pipe processing: Only if commands are on either side of a pipe

Phase 5.5) Execute Redirection:

Phase 6) CALL processing/Caret doubling: Only if the command token is CALL

Phase 7) Execute: The command is executed


Here are details for each phase:

Note that the phases described below are only a model of how the batch parser works. The actual cmd.exe internals may not reflect these phases. But this model is effective at predicting behavior of batch scripts.

Phase 0) Read Line: Read line of input through first <LF>.

  • When reading a line to be parsed as a command, <Ctrl-Z> (0x1A) is read as <LF> (LineFeed 0x0A)
  • When GOTO or CALL reads lines while scanning for a :label, <Ctrl-Z>, is treated as itself — it is not converted to <LF>

Phase 1) Percent Expansion:

  • A double %% is replaced by a single %
  • Expansion of arguments (%*, %1, %2, etc.)
  • Expansion of %var%, if var does not exist replace it with nothing
  • Line is truncated at first <LF> not within %var% expansion
  • For a complete explanation read the first half of this from dbenham Same thread: Percent Phase

Phase 2) Process special characters, tokenize, and build a cached command block: This is a complex process that is affected by things such as quotes, special characters, token delimiters, and caret escapes. What follows is an approximation of this process.

There are concepts that are important throughout this phase.

  • A token is simply a string of characters that is treated as a unit.
  • Tokens are separated by token delimiters. The standard token delimiters are <space> <tab> ; , = <0x0B> <0x0C> and <0xFF>
    Consecutive token delimiters are treated as one — there are no empty tokens between token delimiters
  • There are no token delimiters within a quoted string. The entire quoted string is always treated as part of a single token. A single token may consist of a combination of quoted strings and unquoted characters.

The following characters may have special meaning in this phase, depending on context: <CR> ^ ( @ & | < > <LF> <space> <tab> ; , = <0x0B> <0x0C> <0xFF>

Look at each character from left to right:

  • If <CR> then remove it, as if it were never there (except for weird redirection behavior)
  • If a caret (^), the next character is escaped, and the escaping caret is removed. Escaped characters lose all special meaning (except for <LF>).
  • If a quote ("), toggle the quote flag. If the quote flag is active, then only " and <LF> are special. All other characters lose their special meaning until the next quote toggles the quote flag off. It is not possible to escape the closing quote. All quoted characters are always within the same token.
  • <LF> always turns off the quote flag. Other behaviors vary depending on context, but quotes never alter the behavior of <LF>.
    • Escaped <LF>
      • <LF> is stripped
      • The next character is escaped. If at the end of line buffer, then the next line is read and processed by phases 1 and 1.5 and appended to the current one before escaping the next character. If the next character is <LF>, then it is treated as a literal, meaning this process is not recursive.
    • Unescaped <LF> not within parentheses
      • <LF> is stripped and parsing of the current line is terminated.
      • Any remaining characters in the line buffer are simply ignored.
    • Unescaped <LF> within a FOR IN parenthesized block
      • <LF> is converted into a <space>
      • If at the end of the line buffer, then the next line is read and appended to the current one.
    • Unescaped <LF> within a parenthesized command block
      • <LF> is converted into <LF><space>, and the <space> is treated as part of the next line of the command block.
      • If at the end of line buffer, then the next line is read and appended to the space.
  • If one of the special characters & | < or >, split the line at this point in order to handle pipes, command concatenation, and redirection.
    • In the case of a pipe (|), each side is a separate command (or command block) that gets special handling in phase 5.3
    • In the case of &, &&, or || command concatenation, each side of the concatenation is treated as a separate command.
    • In the case of <, <<, >, or >> redirection, the redirection clause is parsed, temporarily removed, and then appended to the end of the current command. A redirection clause consists of an optional file handle digit, the redirection operator, and the redirection destination token.
      • If the token that precedes the redirection operator is a single unescaped digit, then the digit specifies the file handle to be redirected. If the handle token is not found, then output redirection defaults to 1 (stdout), and input redirection defaults to 0 (stdin).
  • If the very first token for this command (prior to moving redirection to the end) begins with @, then the @ has special meaning. (@ is not special in any other context)
    • The special @ is removed.
    • If ECHO is ON, then this command, along with any following concatenated commands on this line, are excluded from the phase 3 echo. If the @ is before an opening (, then the entire parenthesized block is excluded from the phase 3 echo.
  • Process parenthesis (provides for compound statements across multiple lines):
    • If the parser is not looking for a command token, then ( is not special.
    • If the parser is looking for a command token and finds (, then start a new compound statement and increment the parenthesis counter
    • If the parenthesis counter is > 0 then ) terminates the compound statement and decrements the parenthesis counter.
    • If the line end is reached and the parenthesis counter is > 0 then the next line will be appended to the compound statement (starts again with phase 0)
    • If the parenthesis counter is 0 and the parser is looking for a command, then ) functions similar to a REM statement as long as it is immediately followed by a token delimiter, special character, newline, or end-of-file
      • All special characters lose their meaning except ^ (line concatenation is possible)
      • Once the end of the logical line is reached, the entire «command» is discarded.
  • Each command is parsed into a series of tokens. The first token is always treated as a command token (after special @ have been stripped and redirection moved to the end).
    • Leading token delimiters prior to the command token are stripped
    • When parsing the command token, ( functions as a command token delimiter, in addition to the standard token delimiters
    • The handling of subsequent tokens depends on the command.
  • Most commands simply concatenate all arguments after the command token into a single argument token. All argument token delimiters are preserved. Argument options are typically not parsed until phase 7.
  • Three commands get special handling — IF, FOR, and REM
    • IF is split into two or three distinct parts that are processed independently. A syntax error in the IF construction will result in a fatal syntax error.
      • The comparison operation is the actual command that flows all the way through to phase 7
        • All IF options are fully parsed in phase 2.
        • Consecutive token delimiters collapse into a single space.
        • Depending on the comparison operator, there will be one or two value tokens that are identified.
      • The True command block is the set of commands after the condition, and is parsed like any other command block. If ELSE is to be used, then the True block must be parenthesized.
      • The optional False command block is the set of commands after ELSE. Again, this command block is parsed normally.
      • The True and False command blocks do not automatically flow into the subsequent phases. Their subsequent processing is controled by phase 7.
    • FOR is split in two after the DO. A syntax error in the FOR construction will result in a fatal syntax error.
      • The portion through DO is the actual FOR iteration command that flows all the way through phase 7
        • All FOR options are fully parsed in phase 2.
        • The IN parenthesized clause treats <LF> as <space>. After the IN clause is parsed, all tokens are concatenated together to form a single token.
        • Consecutive unescaped/unquoted token delimiters collapse into a single space throughout the FOR command through DO.
      • The portion after DO is a command block that is parsed normally. Subsequent processing of the DO command block is controled by the iteration in phase 7.
    • REM detected in phase 2 is treated dramatically different than all other commands.
      • Only one argument token is parsed — the parser ignores characters after the first argument token.
      • The REM command may appear in phase 3 output, but the command is never executed, and the original argument text is echoed — escaping carets are not removed, except…
        • If there is only one argument token that ends with an unescaped ^ that ends the line, then the argument token is thrown away, and the subsequent line is parsed and appended to the REM. This repeats until there is more than one token, or the last character is not ^.
  • If the command token begins with :, and this is the first round of phase 2 (not a restart due to CALL in phase 6) then
    • The token is normally treated as an Unexecuted Label.
      • The remainder of the line is parsed, however ), <, >, & and | no longer have special meaning. The entire remainder of the line is considered to be part of the label «command».
      • The ^ continues to be special, meaning that line continuation can be used to append the subsequent line to the label.
      • An Unexecuted Label within a parenthesized block will result in a fatal syntax error unless it is immediately followed by a command or Executed Label on the next line.
        • ( no longer has special meaning for the first command that follows the Unexecuted Label.
      • The command is aborted after label parsing is complete. Subsequent phases do not take place for the label
    • There are three exceptions that can cause a label found in phase 2 to be treated as an Executed Label that continues parsing through phase 7.
      • There is redirection that precedes the label token, and there is a | pipe or &, &&, or || command concatenation on the line.
      • There is redirection that precedes the label token, and the command is within a parenthesized block.
      • The label token is the very first command on a line within a parenthesized block, and the line above ended with an Unexecuted Label.
    • The following occurs when an Executed Label is discovered in phase 2
      • The label, its arguments, and its redirection are all excluded from any echo output in phase 3
      • Any subsequent concatenated commands on the line are fully parsed and executed.
    • For more information about Executed Labels vs. Unexecuted Labels, see https://www.dostips.com/forum/viewtopic.php?f=3&t=3803&p=55405#p55405

Phase 3) Echo the parsed command(s) Only if the command block did not begin with @, and ECHO was ON at the start of the preceding step.

Phase 4) FOR %X variable expansion: Only if a FOR command is active and the commands after DO are being processed.

  • At this point, phase 1 of batch processing will have already converted a FOR variable like %%X into %X. The command line has different percent expansion rules for phase 1. This is the reason that command lines use %X but batch files use %%X for FOR variables.
  • FOR variable names are case sensitive, but ~modifiers are not case sensitive.
  • ~modifiers take precedence over variable names. If a character following ~ is both a modifier and a valid FOR variable name, and there exists a subsequent character that is an active FOR variable name, then the character is interpreted as a modifier.
  • FOR variable names are global, but only within the context of a DO clause. If a routine is CALLed from within a FOR DO clause, then the FOR variables are not expanded within the CALLed routine. But if the routine has its own FOR command, then all currently defined FOR variables are accessible to the inner DO commands.
  • FOR variable names can be reused within nested FORs. The inner FOR value takes precedence, but once the INNER FOR closes, then the outer FOR value is restored.
  • If ECHO was ON at the start of this phase, then phase 3) is repeated to show the parsed DO commands after the FOR variables have been expanded.

—- From this point onward, each command identified in phase 2 is processed separately.
—- Phases 5 through 7 are completed for one command before moving on to the next.

Phase 5) Delayed Expansion: Only if delayed expansion is on, the command is not in a parenthesized block on either side of a pipe, and the command is not a «naked» batch script (script name without parentheses, CALL, command concatenation, or pipe).

  • Each token for a command is parsed for delayed expansion independently.
    • Most commands parse two or more tokens — the command token, the arguments token, and each redirection destination token.
    • The FOR command parses the IN clause token only.
    • The IF command parses the comparison values only — either one or two, depending on the comparison operator.
  • For each parsed token, first check if it contains any !. If not, then the token is not parsed — important for ^ characters.
    If the token does contain !, then scan each character from left to right:

    • If it is a caret (^) the next character has no special meaning, the caret itself is removed
    • If it is an exclamation mark, search for the next exclamation mark (carets are not observed anymore), expand to the value of the variable.
      • Consecutive opening ! are collapsed into a single !
      • Any remaining unpaired ! is removed
    • Expanding vars at this stage is «safe», because special characters are not detected anymore (even <CR> or <LF>)
    • For a more complete explanation, read the 2nd half of this from dbenham
      same thread — Exclamation Point Phase

Phase 5.3) Pipe processing: Only if commands are on either side of a pipe
Each side of the pipe is processed independently and asynchronously.

  • If command is internal to cmd.exe, or it is a batch file, or if it is a parenthesized command block, then it is executed in a new cmd.exe thread via %comspec% /S /D /c" commandBlock", so the command block gets a phase restart, but this time in command line mode.
    • If a parenthesized command block, then all <LF> with a command before and after are converted to <space>&. Other <LF> are stripped.
  • This is the end of processing for the pipe commands.
  • See Why does delayed expansion fail when inside a piped block of code? for more about pipe parsing and processing

Phase 5.5) Execute Redirection: Any redirection that was discovered in phase 2 is now executed.

  • The results of phases 4 and 5 can impact the redirection that was discovered in phase 2.
  • If the redirection fails, then the remainder of the command is aborted. Note that failed redirection does not set ERRORLEVEL to 1 unless || is used.

Phase 6) CALL processing/Caret doubling: Only if the command token is CALL, or if the text before the first occurring standard token delimiter is CALL. If CALL is parsed from a larger command token, then the unused portion is prepended to the arguments token before proceeding.

  • Scan the arguments token for an unquoted /?. If found anywhere within the tokens, then abort phase 6 and proceed to Phase 7, where the HELP for CALL will be printed.
  • Remove the first CALL, so multiple CALL’s can be stacked
  • Double all carets
  • Restart phases 1, 1.5, and 2, but do not continue to phase 3
    • Any doubled carets are reduced back to one caret as long as they are not quoted. But unfortunately, quoted carets remain doubled.
    • Phase 1 changes a bit
      — Expansion errors in step 1.2 or 1.3 abort the CALL, but the error is not fatal — batch processing continues.
    • Phase 2 tasks are altered a bit
      • Any newly appearing unquoted, unescaped redirection that was not detected in the first round of phase 2 is detected, but it is removed (including the file name) without actually performing the redirection
      • Any newly appearing unquoted, unescaped caret at the end of the line is removed without performing line continuation
      • The CALL is aborted without error if any of the following are detected
        • Newly appearing unquoted, unescaped & or |
        • The resultant command token begins with unquoted, unescaped (
        • The very first token after the removed CALL began with @
      • If the resultant command is a seemingly valid IF or FOR, then execution will subsequently fail with an error stating that IF or FOR is not recognized as an internal or external command.
      • Of course the CALL is not aborted in this 2nd round of phase 2 if the resultant command token is a label beginning with :.
  • If the resultant command token is CALL, then restart Phase 6 (repeats until no more CALL)
  • If the resultant command token is a batch script or a :label, then execution of the CALL is fully handled by the remainder of Phase 6.
    • Push the current batch script file position on the call stack so that execution can resume from the correct position when the CALL is completed.
    • Setup the %0, %1, %2, …%N and %* argument tokens for the CALL, using all resultant tokens
    • If the command token is a label that begins with :, then
      • Restart Phase 5. This can impact what :label is CALLed. But since the %0 etc. tokens have already been setup, it will not alter the arguments that are passed to the CALLed routine.
      • Execute GOTO label to position the file pointer at the beginning of the subroutine (ignore any other tokens that may follow the :label) See Phase 7 for rules on how GOTO works.
        • If the :label token is missing, or the :label is not found, then the call stack is immediately popped to restore the saved file position, and the CALL is aborted.
        • If the :label happens to contain /?, then GOTO help is printed instead of searching for the :label. The file pointer does not move, such that code after the CALL is executed twice, once in the CALL context, and then again after the CALL return. See Why CALL prints the GOTO help message in this script?And why command after that are executed twice? for more info.
    • Else transfer control to the specified batch script.
    • Execution of the CALLed :label or script continues until either EXIT /B or end-of-file is reached, at which point the CALL stack is popped and execution resumes from the saved file position.
      Phase 7 is not executed for CALLed scripts or :labels.
  • Else the result of phase 6 falls through into phase 7 for execution.

Phase 7) Execute: The command is executed

  • 7.1 — Execute internal command — If the command token is quoted, then skip this step. Otherwise, attempt to parse out an internal command and execute.
    • The following tests are made to determine if an unquoted command token represents an internal command:
      • If the command token exactly matches an internal command, then execute it.
      • Else break the command token before the first occurrence of + / [ ] <space> <tab> , ; or =
        If the preceding text is an internal command, then remember that command

        • If in command line mode, or if the command is from a parenthesized block, IF true or false command block, FOR DO command block, or involved with command concatenation, then execute the internal command
        • Else (must be a stand-alone command in batch mode) scan the current folder and the PATH for a .COM, .EXE, .BAT, or .CMD file whose base name matches the original command token
          • If the first matching file is a .BAT or .CMD, then goto 7.3.exec and execute that script
          • Else (match not found or first match is .EXE or .COM) execute the remembered internal command
      • Else break the command token before the first occurrence of . or :
        If the preceding text is not an internal command, then goto 7.2
        Else the preceding text may be an internal command. Remember this command.
      • Break the command token before the first occurrence of + / [ ] <space> <tab> , ; or =
        If the preceding text is a path to an existing file, then goto 7.2
        Else execute the remembered internal command.
    • If an internal command is parsed from a larger command token, then the unused portion of the command token is included in the argument list
    • Just because a command token is parsed as an internal command does not mean that it will execute successfully. Each internal command has its own rules as to how the arguments and options are parsed, and what syntax is allowed.
    • All internal commands will print help instead of performing their function if /? is detected. Most recognize /? if it appears anywhere in the arguments. But a few commands like ECHO and SET only print help if the first argument token begins with /?.
    • SET has some interesting semantics:
      • If a SET command has a quote before the variable name and extensions are enabled
        set "name=content" ignored —> value=content
        then the text between the first equal sign and the last quote is used as the content (first equal and last quote excluded). Text after the last quote is ignored. If there is no quote after the equal sign, then the rest of the line is used as content.
      • If a SET command does not have a quote before the name
        set name="content" not ignored —> value="content" not ignored
        then the entire remainder of the line after the equal is used as content, including any and all quotes that may be present.
    • An IF comparison is evaluated, and depending on whether the condition is true or false, the appropriate already parsed dependent command block is processed, starting with phase 5.
    • The IN clause of a FOR command is iterated appropriately.
      • If this is a FOR /F that iterates the output of a command block, then:
        • The IN clause is executed in a new cmd.exe process via CMD /C.
        • The command block must go through the entire parsing process a second time, but this time in a command line context
        • ECHO will start out ON, and delayed expansion will usually start out disabled (dependent on the registry setting)
        • All environment changes made by the IN clause command block will be lost once the child cmd.exe process terminates
      • For each iteration:
        • The FOR variable values are defined
        • The already parsed DO command block is then processed, starting with phase 4.
    • GOTO uses the following logic to locate the :label
      • Parse the label from the first argument token
      • Scan for the next occurrence of the label
        • Start from the current file position
        • If end of file is reached, then loop back to the beginning of file and continue to the original starting point.
      • The scan stops at the first occurrence of the label that it finds, and the file pointer is set to the line immediately following the label. Execution of the script resumes from that point. Note that a successful true GOTO will immediately abort any parsed block of code, including FOR loops.
      • If the label is not found, or the label token is missing, then the GOTO fails, an error message is printed, and the call stack is popped. This effectively functions as an EXIT /B, except any already parsed commands in the current command block that follow the GOTO are still executed, but in the context of the CALLer (the context that exists after EXIT /B)
      • See https://www.dostips.com/forum/viewtopic.php?t=3803 for a more precise description of label parsing rules, and https://www.dostips.com/forum/viewtopic.php?t=8988 for label scanning rules.
    • RENAME and COPY both accept wildcards for the source and target paths. But Microsoft does a terrible job documenting how the wildcards work, especially for the target path. A useful set of wildcard rules may be found at How does the Windows RENAME command interpret wildcards?
  • 7.2 — Execute volume change — Else if the command token does not begin with a quote, is exactly two characters long, and the 2nd character is a colon, then change the volume
    • All argument tokens are ignored
    • If the volume specified by the first character cannot be found, then abort with an error
    • A command token of :: will always result in an error unless SUBST is used to define a volume for ::
      If SUBST is used to define a volume for ::, then the volume will be changed, it will not be treated as a label.
  • 7.3 — Execute external command — Else try to treat the command as an external command.
    • If in command line mode and the command is not quoted and does not begin with a volume specification, white-space, ,, ;, = or + then break the command token at the first occurrence of <space> , ; or = and prepend the remainder to the argument token(s).
    • If the 2nd character of the command token is a colon, then verify the volume specified by the 1st character can be found.
      If the volume cannot be found, then abort with an error.
    • If in batch mode and the command token begins with :, then goto 7.4
      Note that if the label token begins with ::, then this will not be reached because the preceding step will have aborted with an error unless SUBST is used to define a volume for ::.
    • Identify the external command to execute.
      • This is a complex process that may involve the current volume, current directory, PATH variable, PATHEXT variable, and or file associations.
      • If a valid external command cannot be identified, then abort with an error.
    • If in command line mode and the command token begins with :, then goto 7.4
      Note that this is rarely reached because the preceding step will have aborted with an error unless the command token begins with ::, and SUBST is used to define a volume for ::, and the entire command token is a valid path to an external command.
    • 7.3.exec — Execute the external command.
  • 7.4 — Ignore a label — Ignore the command and all its arguments if the command token begins with :.
    Rules in 7.2 and 7.3 may prevent a label from reaching this point.

Command Line Parser:

Works like the BatchLine-Parser, except:

Phase 1) Percent Expansion:

  • No %*, %1 etc. argument expansion
  • If var is undefined, then %var% is left unchanged.
  • No special handling of %%. If var=content, then %%var%% expands to %content%.

Phase 3) Echo the parsed command(s)

  • This is not performed after phase 2. It is only performed after phase 4 for the FOR DO command block.

Phase 5) Delayed Expansion: only if DelayedExpansion is enabled

  • If var is undefined, then !var! is left unchanged.

Phase 7) Execute Command

  • Attempts to CALL or GOTO a :label result in an error.
  • As already documented in phase 7, an executed label may result in an error under different scenarios.
    • Batch executed labels can only cause an error if they begin with ::
    • Command line executed labels almost always result in an error

Parsing of integer values

There are many different contexts where cmd.exe parses integer values from strings, and the rules are inconsistent:

  • SET /A
  • IF
  • %var:~n,m% (variable substring expansion)
  • FOR /F "TOKENS=n"
  • FOR /F "SKIP=n"
  • FOR /L %%A in (n1 n2 n3)
  • EXIT [/B] n

Details for these rules may be found at Rules for how CMD.EXE parses numbers


For anyone wishing to improve the cmd.exe parsing rules, there is a discussion topic on the DosTips forum where issues can be reported and suggestions made.

Hope it helps
Jan Erik (jeb) — Original author and discoverer of phases
Dave Benham (dbenham) — Much additional content and editing

See this page on GitHub


This page is about how ShellCheck reports parser errors, to aid you in finding problems. If you’re getting a parser error for code you know or think is correct, you should submit a bug with an example!

When ShellCheck is unable to parse a file, it’ll output several errors to help pinpoint the problem:

Consider this script, with a missing double quote on line 1:

ssh host "$cmd
echo "Finished"

Bash says:

file: line 2: unexpected EOF while looking for matching `"'
file: line 3: syntax error: unexpected end of file

ShellCheck says:

In file line 1:
ssh host "$cmd
^-- SC1009: The mentioned parser error was in this simple command.

In file line 2:
echo "Finished"
              ^-- SC1073: Couldn't parse this double quoted string.
               ^-- SC1072: Unexpected eof. Fix any mentioned problems and try again.
  1. One error showing the direct problem (SC1072, unexpected eof) (Note: see #1036)
  2. One error showing the construct being parsed (SC1073)
  3. One info showing the outer construct being parsed (SC1009)
  4. Potentially some specific suggestions, such as when missing an fi.

Here, ShellCheck says that the command on line 1 is faulty, which makes it easier to find and fix the actual problem.

Most of ShellCheck’s functionality (specifically, any checks with code >= SC2000) only applies to scripts that parse successfully, so make sure to rerun ShellCheck after fixing any syntax errors.


ShellCheck is a static analysis tool for shell scripts. This page is part of its documentation.

Время прочтения
4 мин

Просмотры 7.7K

Сегодня мы собираемся показать вам, как начать парсинг аргументов командной строки. Кстати, это один пост из серии статей о .NET 5. У нас есть еще много интересного.

Приложения командной строки, также известные как консольные приложения, — это программы, созданные для использования из оболочки, например cmd или bash. Они существуют с 1960-х годов, задолго до появления Windows, MacOS или любого другого графического пользовательского интерфейса (GUI).

Обычно, когда вы начинаете изучать язык программирования, самый простой и распространенный стартовый пример — это приложение Hello world. Подобные примеры в основном выводят на консоль только текст «Hello world», используя свои встроенные API. Компьютерное ПО может делать много разных вещей. Иногда у вас будет ввод, который каким-то образом преобразуется в вывод. В нашем примере «Hello world» нет никакого ввода.

Возьмем C#/.Net. Каждый раз, когда вы создаете новое консольное приложение, вы начинаете с файла Program.cs со статическим методом Main, который является точкой входа в ваше приложение:

...
static void Main(string[] args)
{
    Console.WriteLine("Hello World!");
}
...

Очень важной частью этого кода является определение аргумента string[] args. Это определение параметра, которое содержит все аргументы, которые передаются нашему исполняемому файлу во время инициализации нашего процесса. В отличие от C и C++, имя программы не рассматривается как первый аргумент командной строки в массиве args. Если вам нужно это значение, вы можете вызвать Environment.GetCommandLineArgs().

Если вы привыкли к приложениям командной строки, передача аргументов другим приложениям — очень распространенная задача. Да, вы можете вручную проанализировать эти значения, но если у вас есть несколько параметров, это может быть очень подверженным ошибкам кодом (который в любом случае в основном является шаблонным). Это похоже на проблему, которую кто-то уже мог исправить, не так ли? Поэтому, конечно, мы можем найти библиотеку NuGet, которая поможет нам проанализировать эти аргументы. В этой статье я сосредоточусь на CommandLineParser.

CommandLineParser

CommandLineParser — это библиотека с открытым исходным кодом, созданная Эриком Ньютоном и членами сообщества .NET. Она существует с 2005 года и её скачали более 26 миллионов раз! CommandLineParser «предлагает приложениям CLR простой и лаконичный API для управления аргументами командной строки и связанными задачами, такими как определение переключателей, параметров и команд».

Вместо ручного парсинга массива строк args вы можете просто определить класс, который будет парситься для вас библиотекой на основе набора атрибутов, с которыми вы аннотируете класс.

Вместо того, чтобы создавать еще один пример только для демонстрации этой библиотеки, я буду использовать консольное приложение WinML .NET5, которым я поделился в своем предыдущем посте. Вот исходный код. Начнем с этого и добавим NuGet-пакет CommandLineParser:

Давайте создадим новый класс с именем CommandLineOptions:

using CommandLine;

namespace ImageClassifier
{
    public class CommandLineOptions
    {
        [Value(index: 0, Required = true, HelpText = "Путь к файлу изображения для анализа.")]
        public string Path { get; set; }

        [Option(shortName: 'c', longName: "confidence", Required = false, HelpText = "Minimum confidence.", Default = 0.9f)]
        public float Confidence { get; set; }
    }
}

Это почти все, что нам нужно для использования этой библиотеки. ValueAttribute и OptionAttribute предоставляются пакетом. Я использую именованные параметры, чтобы было ясно, для чего нужен каждый аргумент. Вернемся к нашему методу Program.cs Main, добавим оператор using, чтобы иметь возможность легко использовать классы пакета в этом файле:

using CommandLine;

Давайте изменим тип возвращаемого значения нашего метода Main на Task. Это означает, что любое возвращаемое нами значение int будет возвращено вызывающей стороне нашего процесса, что обычно указывает на успех/неудачу. В этом примере мы просто вернем 0 в случае успеха и любое другое значение, кроме 0, в случае ошибки:

static async Task Main(string[] args)
{
    return await Parser.Default.ParseArguments<CommandLineOptions>(args)
        .MapResult(async (CommandLineOptions opts) =>
        {
            try
            {
                // У нас есть полученные аргументы, поэтому давайте просто передадим их
                return await AnalyzeFileAsync(opts.Path, opts.Confidence);
            }
            catch
            {
                Console.WriteLine("Error!");
                return -3; // Unhandled error
            }
        },
        errs => Task.FromResult(-1)); // Invalid arguments
}

Здесь вы можете увидеть все изменения по сравнению с предыдущей версией кода.

С этими изменениями приложение корректно анализирует наши аргументы. Для нас даже есть страница помощи, созданная автоматически!

Допустим, вы хотите проанализировать изображение, но хотите получить результат, даже если вы не слишком уверены в нем, скажем, с доверием 30%. Теперь это легко сделать с помощью аргумента -c (—confidence). С этим изображением:

Вы можете получить этот результат, используя —confidence:

> .ImageClassifier.exe C:UsersalzollinDownloadsNotALion.jpg --confidence 0.3
Image 'C:UsersalzollinDownloadsNotALion.jpg' is classified as 'Persian cat'(p=58%).

Заключение

Пакет NuGet CommandLineParser — очень мощный помощник, который упрощает эту часто повторяющуюся задачу до простого декларативного подхода. Кроме того, он даже еще более кастомизируемый, чем я продемонстрировал здесь. Вы можете найти его документацию на их странице GitHub вики.

Понравилась статья? Поделить с друзьями:
  • Cmd goto error
  • Cmd fstat error 83 orange 5
  • Cmd exited with error code 9009
  • Cmd exited on with error code 1619
  • Cmd exited on with error code 1603