diff --git a/CHANGELOG.md b/CHANGELOG.md
index 822252c49..f644eae1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# fish 2.6.0 (released ???)
+## Notable fixes and improvements
+
+- Jobs running in the background can now be removed from the list of jobs with the new `disown` builtin, which behaves like the same command in other shells (#2810).
+
+## Other significant changes
+
- The `export` and `setenv` commands now supports colon-separated `PATH`, `CDPATH` and `MANPATH`.
- The `read` command now has a default limit of 10 MiB. If a line is longer than that it will fail with $status set to 122 and the var will be empty. You can set a different limit by setting the FISH_READ_BYTE_LIMIT variable.
- `read` now supports the `--silent` flag to hide the characters typed (#838).
diff --git a/doc_src/disown.txt b/doc_src/disown.txt
new file mode 100644
index 000000000..a8ce16236
--- /dev/null
+++ b/doc_src/disown.txt
@@ -0,0 +1,26 @@
+\section disown disown - remove a process from the list of jobs
+
+\subsection disown-synopsis Synopsis
+\fish{synopsis}
+disown [ PID ... ]
+\endfish
+
+\subsection disown-description Description
+
+`disown` removes the specified job from the list of jobs. The job itself continues to exist, but fish does not keep track of it any longer.
+
+Jobs in the list of jobs are sent a hang-up signal when fish terminates, which usually causes the job to terminate; `disown` allows these processes to continue regardless.
+
+If no process is specified, the most recently-used job is removed (like `bg` and `fg`). If one or more `PID`s are specified, jobs with the specified process IDs are removed from the job list. Invalid jobs are ignored and a warning is printed.
+
+If a job is stopped, it is sent a signal to continue running, and a warning is printed. It is not possible to use the `bg` builtin to continue a job once it has been disowned.
+
+The PID of the desired process is usually found by using process expansion, which can specify jobs or search by process name.
+
+`disown` returns 0 if all specified jobs were disowned successfully, and 1 if any problems were encountered.
+
+\subsection disown-example Example
+
+`firefox &; disown` will start the Firefox web browser in the background and remove it from the job list, meaning it will not be closed when the fish process is closed.
+
+`disown (jobs -p)` removes all jobs from the job list without terminating them.
diff --git a/src/builtin.cpp b/src/builtin.cpp
index e3815ba2f..92d6afdcf 100644
--- a/src/builtin.cpp
+++ b/src/builtin.cpp
@@ -3058,6 +3058,86 @@ static int builtin_bg(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
return res;
}
+/// Helper for builtin_disown
+static int disown_job(parser_t &parser, io_streams_t &streams, job_t *j) {
+ if (j == 0) {
+ streams.err.append_format(_(L"%ls: Unknown job '%ls'\n"), L"bg");
+ builtin_print_help(parser, streams, L"disown", streams.err);
+ return STATUS_BUILTIN_ERROR;
+ }
+
+ // Stopped disowned jobs must be manually signalled; explain how to do so
+ if (job_is_stopped(j)) {
+ killpg(j->pgid, SIGCONT);
+ streams.err.append_format(
+ _(L"%ls: job %d ('%ls') was stopped and has been signalled to continue.\n"),
+ L"disown", j->job_id, j->command_wcstr());
+ }
+
+ if (parser.job_remove(j)) {
+ return STATUS_BUILTIN_OK;
+ } else {
+ return STATUS_BUILTIN_ERROR;
+ }
+}
+
+/// Builtin for removing jobs from the job list
+static int builtin_disown(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
+ int res = STATUS_BUILTIN_OK;
+
+ if (argv[1] == 0) {
+ job_t *j;
+ // Select last constructed job (ie first job in the job queue) that is possible to disown.
+ // Stopped jobs can be disowned (they will be continued).
+ // Foreground jobs can be disowned.
+ // Even jobs that aren't under job control can be disowned!
+ job_iterator_t jobs;
+ while ((j = jobs.next())) {
+ if (j->get_flag(JOB_CONSTRUCTED) && (!job_is_completed(j))) {
+ break;
+ }
+ }
+
+ if (j) {
+ res = disown_job(parser, streams, j);
+ } else {
+ streams.err.append_format(_(L"%ls: There are no suitable jobs\n"), argv[0]);
+ res = STATUS_BUILTIN_ERROR;
+ }
+ } else {
+ std::set jobs;
+
+ // If one argument is not a valid pid (i.e. integer >= 0), fail without disowning anything,
+ // but still print errors for all of them.
+ // Non-existent jobs aren't an error, but information about them is useful.
+ // Multiple PIDs may refer to the same job; include the job only once by using a set.
+ for (int i = 1; argv[i]; i++) {
+ int pid = fish_wcstoi(argv[i]);
+ if (errno || pid < 0) {
+ streams.err.append_format(_(L"%ls: '%ls' is not a valid job specifier\n"), argv[0],
+ argv[i]);
+ res = STATUS_BUILTIN_ERROR;
+ } else {
+ if (job_t *j = parser.job_get_from_pid(pid)) {
+ jobs.insert(j);
+ } else {
+ streams.err.append_format(_(L"%ls: Could not find job '%d'\n"), argv[0], pid);
+ }
+ }
+ }
+ if (res == STATUS_BUILTIN_ERROR) {
+ return res;
+ }
+
+ // Disown all target jobs
+ for (auto j : jobs) {
+ res |= disown_job(parser, streams, j);
+ }
+ }
+
+ return res;
+}
+
/// This function handles both the 'continue' and the 'break' builtins that are used for loop
/// control.
static int builtin_break_continue(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
@@ -3542,6 +3622,7 @@ static const builtin_data_t builtin_datas[] = {
{L"continue", &builtin_break_continue,
N_(L"Skip the rest of the current lap of the innermost loop")},
{L"count", &builtin_count, N_(L"Count the number of arguments")},
+ {L"disown", &builtin_disown, N_(L"Remove job from job list")},
{L"echo", &builtin_echo, N_(L"Print arguments")},
{L"else", &builtin_generic, N_(L"Evaluate block if condition is false")},
{L"emit", &builtin_emit, N_(L"Emit an event")},
diff --git a/tests/jobs.err b/tests/jobs.err
index 5a1052d09..264bfb805 100644
--- a/tests/jobs.err
+++ b/tests/jobs.err
@@ -1,3 +1,4 @@
bg: '-23' is not a valid job specifier
fg: No suitable job: 3
bg: Could not find job '3'
+disown: 'foo' is not a valid job specifier
diff --git a/tests/jobs.in b/tests/jobs.in
index e41b64537..0ddf591dd 100644
--- a/tests/jobs.in
+++ b/tests/jobs.in
@@ -4,4 +4,9 @@ jobs -c
bg -23 1
fg 3
bg 3
+sleep 1 &
+disown
+jobs -c
+disown foo
+disown (jobs -p)
or exit 0
diff --git a/tests/jobs.out b/tests/jobs.out
index 702f017fa..929263874 100644
--- a/tests/jobs.out
+++ b/tests/jobs.out
@@ -1,3 +1,6 @@
Command
sleep
sleep
+Command
+sleep
+sleep