github.com/tompao/docker@v1.9.1/builder/dockerfile/dispatchers.go (about)

     1  package dockerfile
     2  
     3  // This file contains the dispatchers for each command. Note that
     4  // `nullDispatch` is not actually a command, but support for commands we parse
     5  // but do nothing with.
     6  //
     7  // See evaluator.go for a higher level discussion of the whole evaluator
     8  // package.
     9  
    10  import (
    11  	"fmt"
    12  	"io/ioutil"
    13  	"os"
    14  	"path/filepath"
    15  	"regexp"
    16  	"runtime"
    17  	"sort"
    18  	"strings"
    19  
    20  	"github.com/Sirupsen/logrus"
    21  	derr "github.com/docker/docker/errors"
    22  	"github.com/docker/docker/image"
    23  	flag "github.com/docker/docker/pkg/mflag"
    24  	"github.com/docker/docker/pkg/nat"
    25  	"github.com/docker/docker/pkg/signal"
    26  	"github.com/docker/docker/pkg/stringutils"
    27  	"github.com/docker/docker/pkg/system"
    28  	"github.com/docker/docker/runconfig"
    29  )
    30  
    31  const (
    32  	// NoBaseImageSpecifier is the symbol used by the FROM
    33  	// command to specify that no base image is to be used.
    34  	NoBaseImageSpecifier string = "scratch"
    35  )
    36  
    37  // dispatch with no layer / parsing. This is effectively not a command.
    38  func nullDispatch(b *Builder, args []string, attributes map[string]bool, original string) error {
    39  	return nil
    40  }
    41  
    42  // ENV foo bar
    43  //
    44  // Sets the environment variable foo to bar, also makes interpolation
    45  // in the dockerfile available from the next statement on via ${foo}.
    46  //
    47  func env(b *Builder, args []string, attributes map[string]bool, original string) error {
    48  	if len(args) == 0 {
    49  		return derr.ErrorCodeAtLeastOneArg.WithArgs("ENV")
    50  	}
    51  
    52  	if len(args)%2 != 0 {
    53  		// should never get here, but just in case
    54  		return derr.ErrorCodeTooManyArgs.WithArgs("ENV")
    55  	}
    56  
    57  	if err := b.flags.Parse(); err != nil {
    58  		return err
    59  	}
    60  
    61  	// TODO/FIXME/NOT USED
    62  	// Just here to show how to use the builder flags stuff within the
    63  	// context of a builder command. Will remove once we actually add
    64  	// a builder command to something!
    65  	/*
    66  		flBool1 := b.flags.AddBool("bool1", false)
    67  		flStr1 := b.flags.AddString("str1", "HI")
    68  
    69  		if err := b.flags.Parse(); err != nil {
    70  			return err
    71  		}
    72  
    73  		fmt.Printf("Bool1:%v\n", flBool1)
    74  		fmt.Printf("Str1:%v\n", flStr1)
    75  	*/
    76  
    77  	commitStr := "ENV"
    78  
    79  	for j := 0; j < len(args); j++ {
    80  		// name  ==> args[j]
    81  		// value ==> args[j+1]
    82  		newVar := args[j] + "=" + args[j+1] + ""
    83  		commitStr += " " + newVar
    84  
    85  		gotOne := false
    86  		for i, envVar := range b.runConfig.Env {
    87  			envParts := strings.SplitN(envVar, "=", 2)
    88  			if envParts[0] == args[j] {
    89  				b.runConfig.Env[i] = newVar
    90  				gotOne = true
    91  				break
    92  			}
    93  		}
    94  		if !gotOne {
    95  			b.runConfig.Env = append(b.runConfig.Env, newVar)
    96  		}
    97  		j++
    98  	}
    99  
   100  	return b.commit("", b.runConfig.Cmd, commitStr)
   101  }
   102  
   103  // MAINTAINER some text <maybe@an.email.address>
   104  //
   105  // Sets the maintainer metadata.
   106  func maintainer(b *Builder, args []string, attributes map[string]bool, original string) error {
   107  	if len(args) != 1 {
   108  		return derr.ErrorCodeExactlyOneArg.WithArgs("MAINTAINER")
   109  	}
   110  
   111  	if err := b.flags.Parse(); err != nil {
   112  		return err
   113  	}
   114  
   115  	b.maintainer = args[0]
   116  	return b.commit("", b.runConfig.Cmd, fmt.Sprintf("MAINTAINER %s", b.maintainer))
   117  }
   118  
   119  // LABEL some json data describing the image
   120  //
   121  // Sets the Label variable foo to bar,
   122  //
   123  func label(b *Builder, args []string, attributes map[string]bool, original string) error {
   124  	if len(args) == 0 {
   125  		return derr.ErrorCodeAtLeastOneArg.WithArgs("LABEL")
   126  	}
   127  	if len(args)%2 != 0 {
   128  		// should never get here, but just in case
   129  		return derr.ErrorCodeTooManyArgs.WithArgs("LABEL")
   130  	}
   131  
   132  	if err := b.flags.Parse(); err != nil {
   133  		return err
   134  	}
   135  
   136  	commitStr := "LABEL"
   137  
   138  	if b.runConfig.Labels == nil {
   139  		b.runConfig.Labels = map[string]string{}
   140  	}
   141  
   142  	for j := 0; j < len(args); j++ {
   143  		// name  ==> args[j]
   144  		// value ==> args[j+1]
   145  		newVar := args[j] + "=" + args[j+1] + ""
   146  		commitStr += " " + newVar
   147  
   148  		b.runConfig.Labels[args[j]] = args[j+1]
   149  		j++
   150  	}
   151  	return b.commit("", b.runConfig.Cmd, commitStr)
   152  }
   153  
   154  // ADD foo /path
   155  //
   156  // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
   157  // exist here. If you do not wish to have this automatic handling, use COPY.
   158  //
   159  func add(b *Builder, args []string, attributes map[string]bool, original string) error {
   160  	if len(args) < 2 {
   161  		return derr.ErrorCodeAtLeastTwoArgs.WithArgs("ADD")
   162  	}
   163  
   164  	if err := b.flags.Parse(); err != nil {
   165  		return err
   166  	}
   167  
   168  	return b.runContextCommand(args, true, true, "ADD")
   169  }
   170  
   171  // COPY foo /path
   172  //
   173  // Same as 'ADD' but without the tar and remote url handling.
   174  //
   175  func dispatchCopy(b *Builder, args []string, attributes map[string]bool, original string) error {
   176  	if len(args) < 2 {
   177  		return derr.ErrorCodeAtLeastTwoArgs.WithArgs("COPY")
   178  	}
   179  
   180  	if err := b.flags.Parse(); err != nil {
   181  		return err
   182  	}
   183  
   184  	return b.runContextCommand(args, false, false, "COPY")
   185  }
   186  
   187  // FROM imagename
   188  //
   189  // This sets the image the dockerfile will build on top of.
   190  //
   191  func from(b *Builder, args []string, attributes map[string]bool, original string) error {
   192  	if len(args) != 1 {
   193  		return derr.ErrorCodeExactlyOneArg.WithArgs("FROM")
   194  	}
   195  
   196  	if err := b.flags.Parse(); err != nil {
   197  		return err
   198  	}
   199  
   200  	name := args[0]
   201  
   202  	// Windows cannot support a container with no base image.
   203  	if name == NoBaseImageSpecifier {
   204  		if runtime.GOOS == "windows" {
   205  			return fmt.Errorf("Windows does not support FROM scratch")
   206  		}
   207  		b.image = ""
   208  		b.noBaseImage = true
   209  		return nil
   210  	}
   211  
   212  	var (
   213  		image *image.Image
   214  		err   error
   215  	)
   216  	// TODO: don't use `name`, instead resolve it to a digest
   217  	if !b.Pull {
   218  		image, err = b.docker.LookupImage(name)
   219  		// TODO: shouldn't we error out if error is different from "not found" ?
   220  	}
   221  	if image == nil {
   222  		image, err = b.docker.Pull(name)
   223  		if err != nil {
   224  			return err
   225  		}
   226  	}
   227  	return b.processImageFrom(image)
   228  }
   229  
   230  // ONBUILD RUN echo yo
   231  //
   232  // ONBUILD triggers run when the image is used in a FROM statement.
   233  //
   234  // ONBUILD handling has a lot of special-case functionality, the heading in
   235  // evaluator.go and comments around dispatch() in the same file explain the
   236  // special cases. search for 'OnBuild' in internals.go for additional special
   237  // cases.
   238  //
   239  func onbuild(b *Builder, args []string, attributes map[string]bool, original string) error {
   240  	if len(args) == 0 {
   241  		return derr.ErrorCodeAtLeastOneArg.WithArgs("ONBUILD")
   242  	}
   243  
   244  	if err := b.flags.Parse(); err != nil {
   245  		return err
   246  	}
   247  
   248  	triggerInstruction := strings.ToUpper(strings.TrimSpace(args[0]))
   249  	switch triggerInstruction {
   250  	case "ONBUILD":
   251  		return derr.ErrorCodeChainOnBuild
   252  	case "MAINTAINER", "FROM":
   253  		return derr.ErrorCodeBadOnBuildCmd.WithArgs(triggerInstruction)
   254  	}
   255  
   256  	original = regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(original, "")
   257  
   258  	b.runConfig.OnBuild = append(b.runConfig.OnBuild, original)
   259  	return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ONBUILD %s", original))
   260  }
   261  
   262  // WORKDIR /tmp
   263  //
   264  // Set the working directory for future RUN/CMD/etc statements.
   265  //
   266  func workdir(b *Builder, args []string, attributes map[string]bool, original string) error {
   267  	if len(args) != 1 {
   268  		return derr.ErrorCodeExactlyOneArg.WithArgs("WORKDIR")
   269  	}
   270  
   271  	if err := b.flags.Parse(); err != nil {
   272  		return err
   273  	}
   274  
   275  	// This is from the Dockerfile and will not necessarily be in platform
   276  	// specific semantics, hence ensure it is converted.
   277  	workdir := filepath.FromSlash(args[0])
   278  
   279  	if !system.IsAbs(workdir) {
   280  		current := filepath.FromSlash(b.runConfig.WorkingDir)
   281  		workdir = filepath.Join(string(os.PathSeparator), current, workdir)
   282  	}
   283  
   284  	b.runConfig.WorkingDir = workdir
   285  
   286  	return b.commit("", b.runConfig.Cmd, fmt.Sprintf("WORKDIR %v", workdir))
   287  }
   288  
   289  // RUN some command yo
   290  //
   291  // run a command and commit the image. Args are automatically prepended with
   292  // 'sh -c' under linux or 'cmd /S /C' under Windows, in the event there is
   293  // only one argument. The difference in processing:
   294  //
   295  // RUN echo hi          # sh -c echo hi       (Linux)
   296  // RUN echo hi          # cmd /S /C echo hi   (Windows)
   297  // RUN [ "echo", "hi" ] # echo hi
   298  //
   299  func run(b *Builder, args []string, attributes map[string]bool, original string) error {
   300  	if b.image == "" && !b.noBaseImage {
   301  		return derr.ErrorCodeMissingFrom
   302  	}
   303  
   304  	if err := b.flags.Parse(); err != nil {
   305  		return err
   306  	}
   307  
   308  	args = handleJSONArgs(args, attributes)
   309  
   310  	if !attributes["json"] {
   311  		if runtime.GOOS != "windows" {
   312  			args = append([]string{"/bin/sh", "-c"}, args...)
   313  		} else {
   314  			args = append([]string{"cmd", "/S", "/C"}, args...)
   315  		}
   316  	}
   317  
   318  	runCmd := flag.NewFlagSet("run", flag.ContinueOnError)
   319  	runCmd.SetOutput(ioutil.Discard)
   320  	runCmd.Usage = nil
   321  
   322  	config, _, _, err := runconfig.Parse(runCmd, append([]string{b.image}, args...))
   323  	if err != nil {
   324  		return err
   325  	}
   326  
   327  	// stash the cmd
   328  	cmd := b.runConfig.Cmd
   329  	runconfig.Merge(b.runConfig, config)
   330  	// stash the config environment
   331  	env := b.runConfig.Env
   332  
   333  	defer func(cmd *stringutils.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
   334  	defer func(env []string) { b.runConfig.Env = env }(env)
   335  
   336  	// derive the net build-time environment for this run. We let config
   337  	// environment override the build time environment.
   338  	// This means that we take the b.buildArgs list of env vars and remove
   339  	// any of those variables that are defined as part of the container. In other
   340  	// words, anything in b.Config.Env. What's left is the list of build-time env
   341  	// vars that we need to add to each RUN command - note the list could be empty.
   342  	//
   343  	// We don't persist the build time environment with container's config
   344  	// environment, but just sort and prepend it to the command string at time
   345  	// of commit.
   346  	// This helps with tracing back the image's actual environment at the time
   347  	// of RUN, without leaking it to the final image. It also aids cache
   348  	// lookup for same image built with same build time environment.
   349  	cmdBuildEnv := []string{}
   350  	configEnv := runconfig.ConvertKVStringsToMap(b.runConfig.Env)
   351  	for key, val := range b.BuildArgs {
   352  		if !b.isBuildArgAllowed(key) {
   353  			// skip build-args that are not in allowed list, meaning they have
   354  			// not been defined by an "ARG" Dockerfile command yet.
   355  			// This is an error condition but only if there is no "ARG" in the entire
   356  			// Dockerfile, so we'll generate any necessary errors after we parsed
   357  			// the entire file (see 'leftoverArgs' processing in evaluator.go )
   358  			continue
   359  		}
   360  		if _, ok := configEnv[key]; !ok {
   361  			cmdBuildEnv = append(cmdBuildEnv, fmt.Sprintf("%s=%s", key, val))
   362  		}
   363  	}
   364  
   365  	// derive the command to use for probeCache() and to commit in this container.
   366  	// Note that we only do this if there are any build-time env vars.  Also, we
   367  	// use the special argument "|#" at the start of the args array. This will
   368  	// avoid conflicts with any RUN command since commands can not
   369  	// start with | (vertical bar). The "#" (number of build envs) is there to
   370  	// help ensure proper cache matches. We don't want a RUN command
   371  	// that starts with "foo=abc" to be considered part of a build-time env var.
   372  	saveCmd := config.Cmd
   373  	if len(cmdBuildEnv) > 0 {
   374  		sort.Strings(cmdBuildEnv)
   375  		tmpEnv := append([]string{fmt.Sprintf("|%d", len(cmdBuildEnv))}, cmdBuildEnv...)
   376  		saveCmd = stringutils.NewStrSlice(append(tmpEnv, saveCmd.Slice()...)...)
   377  	}
   378  
   379  	b.runConfig.Cmd = saveCmd
   380  	hit, err := b.probeCache()
   381  	if err != nil {
   382  		return err
   383  	}
   384  	if hit {
   385  		return nil
   386  	}
   387  
   388  	// set Cmd manually, this is special case only for Dockerfiles
   389  	b.runConfig.Cmd = config.Cmd
   390  	// set build-time environment for 'run'.
   391  	b.runConfig.Env = append(b.runConfig.Env, cmdBuildEnv...)
   392  
   393  	logrus.Debugf("[BUILDER] Command to be executed: %v", b.runConfig.Cmd)
   394  
   395  	c, err := b.create()
   396  	if err != nil {
   397  		return err
   398  	}
   399  
   400  	// Ensure that we keep the container mounted until the commit
   401  	// to avoid unmounting and then mounting directly again
   402  	c.Mount()
   403  	defer c.Unmount()
   404  
   405  	err = b.run(c)
   406  	if err != nil {
   407  		return err
   408  	}
   409  
   410  	// revert to original config environment and set the command string to
   411  	// have the build-time env vars in it (if any) so that future cache look-ups
   412  	// properly match it.
   413  	b.runConfig.Env = env
   414  	b.runConfig.Cmd = saveCmd
   415  	if err := b.commit(c.ID, cmd, "run"); err != nil {
   416  		return err
   417  	}
   418  
   419  	return nil
   420  }
   421  
   422  // CMD foo
   423  //
   424  // Set the default command to run in the container (which may be empty).
   425  // Argument handling is the same as RUN.
   426  //
   427  func cmd(b *Builder, args []string, attributes map[string]bool, original string) error {
   428  	if err := b.flags.Parse(); err != nil {
   429  		return err
   430  	}
   431  
   432  	cmdSlice := handleJSONArgs(args, attributes)
   433  
   434  	if !attributes["json"] {
   435  		if runtime.GOOS != "windows" {
   436  			cmdSlice = append([]string{"/bin/sh", "-c"}, cmdSlice...)
   437  		} else {
   438  			cmdSlice = append([]string{"cmd", "/S", "/C"}, cmdSlice...)
   439  		}
   440  	}
   441  
   442  	b.runConfig.Cmd = stringutils.NewStrSlice(cmdSlice...)
   443  
   444  	if err := b.commit("", b.runConfig.Cmd, fmt.Sprintf("CMD %q", cmdSlice)); err != nil {
   445  		return err
   446  	}
   447  
   448  	if len(args) != 0 {
   449  		b.cmdSet = true
   450  	}
   451  
   452  	return nil
   453  }
   454  
   455  // ENTRYPOINT /usr/sbin/nginx
   456  //
   457  // Set the entrypoint (which defaults to sh -c on linux, or cmd /S /C on Windows) to
   458  // /usr/sbin/nginx. Will accept the CMD as the arguments to /usr/sbin/nginx.
   459  //
   460  // Handles command processing similar to CMD and RUN, only b.runConfig.Entrypoint
   461  // is initialized at NewBuilder time instead of through argument parsing.
   462  //
   463  func entrypoint(b *Builder, args []string, attributes map[string]bool, original string) error {
   464  	if err := b.flags.Parse(); err != nil {
   465  		return err
   466  	}
   467  
   468  	parsed := handleJSONArgs(args, attributes)
   469  
   470  	switch {
   471  	case attributes["json"]:
   472  		// ENTRYPOINT ["echo", "hi"]
   473  		b.runConfig.Entrypoint = stringutils.NewStrSlice(parsed...)
   474  	case len(parsed) == 0:
   475  		// ENTRYPOINT []
   476  		b.runConfig.Entrypoint = nil
   477  	default:
   478  		// ENTRYPOINT echo hi
   479  		if runtime.GOOS != "windows" {
   480  			b.runConfig.Entrypoint = stringutils.NewStrSlice("/bin/sh", "-c", parsed[0])
   481  		} else {
   482  			b.runConfig.Entrypoint = stringutils.NewStrSlice("cmd", "/S /C", parsed[0])
   483  		}
   484  	}
   485  
   486  	// when setting the entrypoint if a CMD was not explicitly set then
   487  	// set the command to nil
   488  	if !b.cmdSet {
   489  		b.runConfig.Cmd = nil
   490  	}
   491  
   492  	if err := b.commit("", b.runConfig.Cmd, fmt.Sprintf("ENTRYPOINT %q", b.runConfig.Entrypoint)); err != nil {
   493  		return err
   494  	}
   495  
   496  	return nil
   497  }
   498  
   499  // EXPOSE 6667/tcp 7000/tcp
   500  //
   501  // Expose ports for links and port mappings. This all ends up in
   502  // b.runConfig.ExposedPorts for runconfig.
   503  //
   504  func expose(b *Builder, args []string, attributes map[string]bool, original string) error {
   505  	portsTab := args
   506  
   507  	if len(args) == 0 {
   508  		return derr.ErrorCodeAtLeastOneArg.WithArgs("EXPOSE")
   509  	}
   510  
   511  	if err := b.flags.Parse(); err != nil {
   512  		return err
   513  	}
   514  
   515  	if b.runConfig.ExposedPorts == nil {
   516  		b.runConfig.ExposedPorts = make(nat.PortSet)
   517  	}
   518  
   519  	ports, _, err := nat.ParsePortSpecs(portsTab)
   520  	if err != nil {
   521  		return err
   522  	}
   523  
   524  	// instead of using ports directly, we build a list of ports and sort it so
   525  	// the order is consistent. This prevents cache burst where map ordering
   526  	// changes between builds
   527  	portList := make([]string, len(ports))
   528  	var i int
   529  	for port := range ports {
   530  		if _, exists := b.runConfig.ExposedPorts[port]; !exists {
   531  			b.runConfig.ExposedPorts[port] = struct{}{}
   532  		}
   533  		portList[i] = string(port)
   534  		i++
   535  	}
   536  	sort.Strings(portList)
   537  	return b.commit("", b.runConfig.Cmd, fmt.Sprintf("EXPOSE %s", strings.Join(portList, " ")))
   538  }
   539  
   540  // USER foo
   541  //
   542  // Set the user to 'foo' for future commands and when running the
   543  // ENTRYPOINT/CMD at container run time.
   544  //
   545  func user(b *Builder, args []string, attributes map[string]bool, original string) error {
   546  	if len(args) != 1 {
   547  		return derr.ErrorCodeExactlyOneArg.WithArgs("USER")
   548  	}
   549  
   550  	if err := b.flags.Parse(); err != nil {
   551  		return err
   552  	}
   553  
   554  	b.runConfig.User = args[0]
   555  	return b.commit("", b.runConfig.Cmd, fmt.Sprintf("USER %v", args))
   556  }
   557  
   558  // VOLUME /foo
   559  //
   560  // Expose the volume /foo for use. Will also accept the JSON array form.
   561  //
   562  func volume(b *Builder, args []string, attributes map[string]bool, original string) error {
   563  	if len(args) == 0 {
   564  		return derr.ErrorCodeAtLeastOneArg.WithArgs("VOLUME")
   565  	}
   566  
   567  	if err := b.flags.Parse(); err != nil {
   568  		return err
   569  	}
   570  
   571  	if b.runConfig.Volumes == nil {
   572  		b.runConfig.Volumes = map[string]struct{}{}
   573  	}
   574  	for _, v := range args {
   575  		v = strings.TrimSpace(v)
   576  		if v == "" {
   577  			return derr.ErrorCodeVolumeEmpty
   578  		}
   579  		b.runConfig.Volumes[v] = struct{}{}
   580  	}
   581  	if err := b.commit("", b.runConfig.Cmd, fmt.Sprintf("VOLUME %v", args)); err != nil {
   582  		return err
   583  	}
   584  	return nil
   585  }
   586  
   587  // STOPSIGNAL signal
   588  //
   589  // Set the signal that will be used to kill the container.
   590  func stopSignal(b *Builder, args []string, attributes map[string]bool, original string) error {
   591  	if len(args) != 1 {
   592  		return fmt.Errorf("STOPSIGNAL requires exactly one argument")
   593  	}
   594  
   595  	sig := args[0]
   596  	_, err := signal.ParseSignal(sig)
   597  	if err != nil {
   598  		return err
   599  	}
   600  
   601  	b.runConfig.StopSignal = sig
   602  	return b.commit("", b.runConfig.Cmd, fmt.Sprintf("STOPSIGNAL %v", args))
   603  }
   604  
   605  // ARG name[=value]
   606  //
   607  // Adds the variable foo to the trusted list of variables that can be passed
   608  // to builder using the --build-arg flag for expansion/subsitution or passing to 'run'.
   609  // Dockerfile author may optionally set a default value of this variable.
   610  func arg(b *Builder, args []string, attributes map[string]bool, original string) error {
   611  	if len(args) != 1 {
   612  		return fmt.Errorf("ARG requires exactly one argument definition")
   613  	}
   614  
   615  	var (
   616  		name       string
   617  		value      string
   618  		hasDefault bool
   619  	)
   620  
   621  	arg := args[0]
   622  	// 'arg' can just be a name or name-value pair. Note that this is different
   623  	// from 'env' that handles the split of name and value at the parser level.
   624  	// The reason for doing it differently for 'arg' is that we support just
   625  	// defining an arg and not assign it a value (while 'env' always expects a
   626  	// name-value pair). If possible, it will be good to harmonize the two.
   627  	if strings.Contains(arg, "=") {
   628  		parts := strings.SplitN(arg, "=", 2)
   629  		name = parts[0]
   630  		value = parts[1]
   631  		hasDefault = true
   632  	} else {
   633  		name = arg
   634  		hasDefault = false
   635  	}
   636  	// add the arg to allowed list of build-time args from this step on.
   637  	b.allowedBuildArgs[name] = true
   638  
   639  	// If there is a default value associated with this arg then add it to the
   640  	// b.buildArgs if one is not already passed to the builder. The args passed
   641  	// to builder override the defaut value of 'arg'.
   642  	if _, ok := b.BuildArgs[name]; !ok && hasDefault {
   643  		b.BuildArgs[name] = value
   644  	}
   645  
   646  	return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ARG %s", arg))
   647  }