@@ -73,6 +73,7 @@ func newShellCommand() *cobra.Command {
7373 shellCmd .Flags ().Bool ("preserve-env" , false , "Propagate environment variables to the shell" )
7474 shellCmd .Flags ().Bool ("start" , false , "Start the instance if it is not already running" )
7575 shellCmd .Flags ().String ("sync" , "" , "Copy a host directory to the guest and vice-versa upon exit" )
76+ shellCmd .Flags ().StringArray ("sync-exclude" , nil , "Exclude pattern for --sync (can be specified multiple times)" )
7677
7778 return shellCmd
7879}
@@ -196,10 +197,22 @@ func shellAction(cmd *cobra.Command, args []string) error {
196197 return fmt .Errorf ("failed to get sync flag: %w" , err )
197198 }
198199 syncHostWorkdir := syncDirVal != ""
200+ // Validate that --sync has a proper directory value, not another flag
201+ if err := validateSyncFlagValue (syncDirVal ); err != nil {
202+ return err
203+ }
199204 if syncHostWorkdir && len (inst .Config .Mounts ) > 0 {
200205 return errors .New ("cannot use `--sync` when the instance has host mounts configured, start the instance with `--mount-none` to disable mounts" )
201206 }
202207
208+ syncExcludes , err := flags .GetStringArray ("sync-exclude" )
209+ if err != nil {
210+ return fmt .Errorf ("failed to get sync-exclude flag: %w" , err )
211+ }
212+ if len (syncExcludes ) > 0 && ! syncHostWorkdir {
213+ return errors .New ("cannot use `--sync-exclude` without `--sync`" )
214+ }
215+
203216 // When workDir is explicitly set, the shell MUST have workDir as the cwd, or exit with an error.
204217 //
205218 // changeDirCmd := "cd workDir || exit 1" if workDir != ""
@@ -354,6 +367,7 @@ func shellAction(cmd *cobra.Command, args []string) error {
354367 var (
355368 sshExecForRsync * exec.Cmd
356369 rsync copytool.CopyTool
370+ excludeArgs []string
357371 )
358372 if syncHostWorkdir {
359373 logrus .Infof ("Syncing host current directory(%s) to guest instance..." , hostCurrentDir )
@@ -380,12 +394,13 @@ func shellAction(cmd *cobra.Command, args []string) error {
380394 hostCurrentDir ,
381395 fmt .Sprintf ("%s:%s" , inst .Name , destRsyncDir ),
382396 }
397+ excludeArgs = buildSyncExcludeArgs (syncExcludes , hostCurrentDir )
383398 rsync , err = copytool .New (ctx , string (copytool .BackendRsync ), paths , & copytool.Options {
384399 Recursive : true ,
385400 Verbose : false ,
386- AdditionalArgs : []string {
401+ AdditionalArgs : append ( []string {
387402 "--delete" ,
388- },
403+ }, excludeArgs ... ),
389404 })
390405 if err != nil {
391406 return err
@@ -439,12 +454,12 @@ func shellAction(cmd *cobra.Command, args []string) error {
439454 if err != nil {
440455 return err
441456 }
442- return askUserForRsyncBack (ctx , cmd , inst , sshExecForRsync , hostCurrentDir , destRsyncDir , rsync , tty )
457+ return askUserForRsyncBack (ctx , cmd , inst , sshExecForRsync , hostCurrentDir , destRsyncDir , rsync , tty , excludeArgs )
443458 }
444459 return nil
445460}
446461
447- func askUserForRsyncBack (ctx context.Context , cmd * cobra.Command , inst * limatype.Instance , sshCmd * exec.Cmd , hostCurrentDir , destRsyncDir string , rsync copytool.CopyTool , tty bool ) error {
462+ func askUserForRsyncBack (ctx context.Context , cmd * cobra.Command , inst * limatype.Instance , sshCmd * exec.Cmd , hostCurrentDir , destRsyncDir string , rsync copytool.CopyTool , tty bool , excludeArgs [] string ) error {
448463 remoteSource := fmt .Sprintf ("%s:%s" , inst .Name , destRsyncDir )
449464 clean := filepath .Clean (hostCurrentDir )
450465 dirForCleanup := shellescape .Quote (filepath .Join (* inst .Config .User .Home , clean ))
@@ -473,7 +488,7 @@ func askUserForRsyncBack(ctx context.Context, cmd *cobra.Command, inst *limatype
473488 return rsyncBack ()
474489 }
475490
476- rawOutput , stats , err := getRsyncStats (ctx , remoteSource , filepath .Dir (hostCurrentDir ))
491+ rawOutput , stats , err := getRsyncStats (ctx , remoteSource , filepath .Dir (hostCurrentDir ), excludeArgs )
477492 if err != nil {
478493 logrus .WithError (err ).Warn ("failed to get rsync stats" )
479494 }
@@ -742,16 +757,16 @@ func (s *rsyncStats) String() string {
742757 return fmt .Sprintf ("added: %d, deleted: %d, modified: %d, metadata: %d" , s .Added , s .Deleted , s .Modified , s .Metadata )
743758}
744759
745- func getRsyncStats (ctx context.Context , source , destination string ) (string , * rsyncStats , error ) {
760+ func getRsyncStats (ctx context.Context , source , destination string , excludeArgs [] string ) (string , * rsyncStats , error ) {
746761 paths := []string {source , destination }
747762 rsync , err := copytool .New (ctx , string (copytool .BackendRsync ), paths , & copytool.Options {
748763 Verbose : true ,
749- AdditionalArgs : []string {
764+ AdditionalArgs : append ( []string {
750765 "--dry-run" ,
751766 "--itemize-changes" ,
752767 "-ah" ,
753768 "--delete" ,
754- },
769+ }, excludeArgs ... ),
755770 })
756771 if err != nil {
757772 return "" , nil , err
@@ -822,6 +837,29 @@ func parseRsyncStats(output string) *rsyncStats {
822837 return & s
823838}
824839
840+ // buildSyncExcludeArgs converts --sync-exclude flag values and an optional
841+ // .limasyncignore file into rsync --exclude / --exclude-from arguments.
842+ func buildSyncExcludeArgs (excludes []string , syncDir string ) []string {
843+ var args []string
844+ for _ , pattern := range excludes {
845+ args = append (args , "--exclude" , pattern )
846+ }
847+ ignoreFile := filepath .Join (syncDir , ".limasyncignore" )
848+ if _ , err := os .Stat (ignoreFile ); err == nil {
849+ args = append (args , "--exclude-from" , ignoreFile )
850+ }
851+ return args
852+ }
853+
854+ // validateSyncFlagValue ensures the value passed to --sync is a directory
855+ // path and not another flag token like "--sync-exclude=...".
856+ func validateSyncFlagValue (syncDirVal string ) error {
857+ if syncDirVal != "" && strings .HasPrefix (syncDirVal , "--" ) {
858+ return fmt .Errorf ("--sync flag requires a directory path argument, got %q instead\n Usage: limactl shell --sync <DIR> [--sync-exclude <PATTERN>...] <INSTANCE> [COMMAND...]" , syncDirVal )
859+ }
860+ return nil
861+ }
862+
825863func hasMetadataDelta (attrs string ) bool {
826864 for _ , ch := range attrs {
827865 if ch != '.' {
0 commit comments