Repository: peak/s5cmd (go)

Issue:
Flatten list?
I see you have `--flatten` with `cp` but it seems like it's not supported for `ls`? 

I might be able to do a pull request to add this, it would be really useful for deeply nested directories (not my idea to store the data this way, I just get paid to clean it up). 

Pull request description:
ls: add `--show-fullpath` flag
Resolves #596 .

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd94c13..28188e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 #### Breaking changes
 #### Features
+-- Added `--show-fullpath` flag to `ls`. (#[596](https://github.com/peak/s5cmd/issues/596))
 #### Improvements
 #### Bugfixes
 - Fixed a bug introduced with `external sort` support in `sync` command which prevents `sync` to an empty destination with `--delete` option. ([#576](https://github.com/peak/s5cmd/issues/576))
diff --git a/command/ls.go b/command/ls.go
index ae14519..7d8cc3d 100644
--- a/command/ls.go
+++ b/command/ls.go
@@ -55,6 +55,9 @@ Examples:
 	10. List all versions of all objects in the bucket
 		 > s5cmd {{.HelpName}} --all-versions "s3://bucket/*"
 
+	11. List all files only with their fullpaths 
+		 > s5cmd {{.HelpName}} --show-fullpath "s3://bucket/*"
+
 `
 
 func NewListCommand() *cli.Command {
@@ -87,6 +90,10 @@ func NewListCommand() *cli.Command {
 				Name:  "all-versions",
 				Usage: "list all versions of object(s)",
 			},
+			&cli.BoolFlag{
+				Name:  "show-fullpath",
+				Usage: "shows only the fullpath names of the object(s)",
+			},
 		},
 		Before: func(c *cli.Context) error {
 			err := validateLSCommand(c)
@@ -122,6 +129,7 @@ func NewListCommand() *cli.Command {
 				humanize:         c.Bool("humanize"),
 				showStorageClass: c.Bool("storage-class"),
 				exclude:          c.StringSlice("exclude"),
+				showFullPath:     c.Bool("show-fullpath"),
 
 				storageOpts: NewStorageOpts(c),
 			}.Run(c.Context)
@@ -142,6 +150,7 @@ type List struct {
 	showEtag         bool
 	humanize         bool
 	showStorageClass bool
+	showFullPath     bool
 	exclude          []string
 
 	storageOpts storage.Options
@@ -205,6 +214,7 @@ func (l List) Run(ctx context.Context) error {
 			showEtag:         l.showEtag,
 			showHumanized:    l.humanize,
 			showStorageClass: l.showStorageClass,
+			showFullPath:     l.showFullPath,
 		}
 
 		log.Info(msg)
@@ -220,6 +230,7 @@ type ListMessage struct {
 	showEtag         bool
 	showHumanized    bool
 	showStorageClass bool
+	showFullPath     bool
 }
 
 // humanize is a helper function to humanize bytes.
@@ -239,6 +250,9 @@ const (
 
 // String returns the string representation of ListMessage.
 func (l ListMessage) String() string {
+	if l.showFullPath {
+		return l.Object.URL.String()
+	}
 	var etag string
 	// date and storage fiels
 	var listFormat = "%19s %2s"
@@ -278,7 +292,6 @@ func (l ListMessage) String() string {
 	if l.showStorageClass {
 		stclass = fmt.Sprintf("%v", l.Object.StorageClass)
 	}
-
 	s = fmt.Sprintf(
 		listFormat,
 		l.Object.ModTime.Format(dateFormat),


Test patch:
diff --git a/e2e/ls_test.go b/e2e/ls_test.go
index 42351bb..3fa176d 100644
--- a/e2e/ls_test.go
+++ b/e2e/ls_test.go
@@ -291,6 +291,44 @@ func TestListS3ObjectsAndFolders(t *testing.T) {
 	}, alignment(true))
 }
 
+func TestListS3ObjectsAndFoldersWithTheirFullpath(t *testing.T) {
+	t.Parallel()
+
+	s3client, s5cmd := setup(t)
+
+	bucket := s3BucketFromTestName(t)
+	createBucket(t, s3client, bucket)
+	putFile(t, s3client, bucket, "testfile1.txt", "content")
+	putFile(t, s3client, bucket, "report.gz", "content")
+	putFile(t, s3client, bucket, "a/testfile2.txt", "content")
+	putFile(t, s3client, bucket, "b/testfile3.txt", "content")
+	putFile(t, s3client, bucket, "b/testfile4.txt", "content")
+	putFile(t, s3client, bucket, "c/testfile5.gz", "content")
+	putFile(t, s3client, bucket, "d/foo/bar/file7.txt", "content")
+	putFile(t, s3client, bucket, "d/foo/bar/testfile8.txt", "content")
+	putFile(t, s3client, bucket, "e/txt/testfile9.txt.gz", "content")
+	putFile(t, s3client, bucket, "f/txt/testfile10.txt", "content")
+
+	cmd := s5cmd("ls", "--show-fullpath", "s3://"+bucket+"/*")
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+
+	// assert lexical order
+	assertLines(t, result.Stdout(), map[int]compareFunc{
+		0: equals("s3://%v/a/testfile2.txt", bucket),
+		1: equals("s3://%v/b/testfile3.txt", bucket),
+		2: equals("s3://%v/b/testfile4.txt", bucket),
+		3: equals("s3://%v/c/testfile5.gz", bucket),
+		4: equals("s3://%v/d/foo/bar/file7.txt", bucket),
+		5: equals("s3://%v/d/foo/bar/testfile8.txt", bucket),
+		6: equals("s3://%v/e/txt/testfile9.txt.gz", bucket),
+		7: equals("s3://%v/f/txt/testfile10.txt", bucket),
+		8: equals("s3://%v/report.gz", bucket),
+		9: equals("s3://%v/testfile1.txt", bucket),
+	}, alignment(true))
+}
+
 // ls bucket/prefix
 func TestListS3ObjectsAndFoldersWithPrefix(t *testing.T) {
 	t.Parallel()