Skip to content

Commit 043ab63

Browse files
create --paths-from-shell-command, fixes #5968
This adds the `--paths-from-shell-command` option to the `create` command, enabling the use of shell-specific features like pipes and redirection when specifying input paths. Includes related test coverage.
1 parent 41f7d9e commit 043ab63

File tree

2 files changed

+35
-4
lines changed

2 files changed

+35
-4
lines changed

src/borg/archiver/create_cmd.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,24 @@ def create_inner(archive, cache, fso):
9191
else:
9292
status = "+" # included
9393
self.print_file_status(status, path)
94-
elif args.paths_from_command or args.paths_from_stdin:
94+
elif args.paths_from_command or args.paths_from_shell_command or args.paths_from_stdin:
9595
paths_sep = eval_escapes(args.paths_delimiter) if args.paths_delimiter is not None else "\n"
96-
if args.paths_from_command:
96+
if args.paths_from_command or args.paths_from_shell_command:
9797
try:
9898
env = prepare_subprocess_env(system=True)
99+
if args.paths_from_shell_command:
100+
# Use shell=True to support pipes, redirection, etc.
101+
shell = True
102+
cmd = " ".join(args.paths)
103+
else:
104+
shell = False
105+
cmd = args.paths
99106
proc = subprocess.Popen( # nosec B603
100-
args.paths, stdout=subprocess.PIPE, env=env, preexec_fn=None if is_win32 else ignore_sigint
107+
cmd,
108+
stdout=subprocess.PIPE,
109+
env=env,
110+
shell=shell,
111+
preexec_fn=None if is_win32 else ignore_sigint,
101112
)
102113
except (FileNotFoundError, PermissionError) as e:
103114
raise CommandError(f"Failed to execute command: {e}")
@@ -131,7 +142,7 @@ def create_inner(archive, cache, fso):
131142
self.print_file_status(status, path)
132143
if not dry_run and status is not None:
133144
fso.stats.files_stats[status] += 1
134-
if args.paths_from_command:
145+
if args.paths_from_command or args.paths_from_shell_command:
135146
rc = proc.wait()
136147
if rc != 0:
137148
raise CommandError(f"Command {args.paths[0]!r} exited with status {rc}")
@@ -844,6 +855,11 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser):
844855
action="store_true",
845856
help="interpret PATH as command and treat its output as ``--paths-from-stdin``",
846857
)
858+
subparser.add_argument(
859+
"--paths-from-shell-command",
860+
action="store_true",
861+
help="interpret PATH as a shell command (be careful!) and treat its output as ``--paths-from-stdin``",
862+
)
847863
subparser.add_argument(
848864
"--paths-delimiter",
849865
action=Highlander,

src/borg/testsuite/archiver/create_cmd_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,21 @@ def test_create_paths_from_command_missing_command(archivers, request):
418418
assert output.endswith("No command given." + os.linesep)
419419

420420

421+
@pytest.mark.skipif(is_win32, reason="shell patterns not supported on Windows")
422+
def test_create_paths_from_shell_command(archivers, request):
423+
archiver = request.getfixturevalue(archivers)
424+
cmd(archiver, "repo-create", RK_ENCRYPTION)
425+
create_regular_file(archiver.input_path, "file1", size=1024 * 80)
426+
create_regular_file(archiver.input_path, "file2", size=1024 * 80)
427+
create_regular_file(archiver.input_path, "file3", size=1024 * 80)
428+
input_data = "input/file1\ninput/file2\ninput/file3"
429+
# Use a shell pipe to test that shell=True works correctly.
430+
cmd(archiver, "create", "--paths-from-shell-command", "test", "--", f"echo '{input_data}' | head -n 2")
431+
archive_list = cmd(archiver, "list", "test", "--json-lines")
432+
paths = [json.loads(line)["path"] for line in archive_list.split("\n") if line]
433+
assert paths == ["input/file1", "input/file2"]
434+
435+
421436
def test_create_without_root(archivers, request):
422437
"""test create without a root"""
423438
archiver = request.getfixturevalue(archivers)

0 commit comments

Comments
 (0)