Merge nucleic/sleek-thistle-egret-fyej into dev
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
[package]
|
||||
name = "brush-shell"
|
||||
description = "Rust-implemented shell focused on POSIX and bash compatibility"
|
||||
version = "0.4.0"
|
||||
authors.workspace = true
|
||||
categories.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.binstall]
|
||||
pkg-url = "{ repo }/releases/download/{ name }-v{ version }/brush-{ target }{ archive-suffix }"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "brush"
|
||||
path = "src/main.rs"
|
||||
bench = false
|
||||
|
||||
[[test]]
|
||||
name = "brush-compat-tests"
|
||||
path = "tests/compat_tests.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "brush-integration-tests"
|
||||
path = "tests/integration_tests.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "brush-version-tests"
|
||||
path = "tests/version_tests.rs"
|
||||
|
||||
[[test]]
|
||||
name = "brush-interactive-tests"
|
||||
path = "tests/interactive_tests.rs"
|
||||
|
||||
[[test]]
|
||||
name = "brush-completion-tests"
|
||||
path = "tests/completion_tests.rs"
|
||||
|
||||
[[bench]]
|
||||
name = "shell"
|
||||
path = "benches/shell.rs"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = ["basic", "reedline", "minimal"]
|
||||
basic = ["brush-interactive/basic"]
|
||||
minimal = ["brush-interactive/minimal"]
|
||||
reedline = ["brush-interactive/reedline"]
|
||||
experimental = [
|
||||
"experimental-builtins",
|
||||
"experimental-bundled-coreutils",
|
||||
"experimental-load",
|
||||
"experimental-parser",
|
||||
]
|
||||
experimental-builtins = ["dep:brush-experimental-builtins"]
|
||||
experimental-bundled-coreutils = [
|
||||
"dep:brush-coreutils-builtins",
|
||||
"brush-coreutils-builtins/coreutils.all",
|
||||
]
|
||||
experimental-load = ["dep:serde_json", "brush-core/serde"]
|
||||
experimental-parser = [
|
||||
"brush-core/experimental-parser",
|
||||
"brush-parser/winnow-parser",
|
||||
]
|
||||
schema = ["dep:schemars"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
brush-core = { version = "^0.5.0", path = "../brush-core" }
|
||||
brush-builtins = { version = "^0.2.0", path = "../brush-builtins" }
|
||||
brush-coreutils-builtins = { version = "^0.1.0", path = "../brush-coreutils-builtins", optional = true }
|
||||
brush-experimental-builtins = { version = "^0.1.0", path = "../brush-experimental-builtins", optional = true }
|
||||
clap = { version = "4.6.0", features = ["derive", "env"] }
|
||||
color-print = "0.3.7"
|
||||
etcetera = "0.11.0"
|
||||
schemars = { version = "1.2.1", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "1.1.0"
|
||||
const_format = "0.2.36"
|
||||
git-version = "0.3.9"
|
||||
serde_json = { version = "1.0.149", optional = true }
|
||||
thiserror = "2.0.18"
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.23"
|
||||
human-panic = "2.0.6"
|
||||
|
||||
[target.'cfg(not(any(unix, windows)))'.dependencies]
|
||||
brush-interactive = { version = "^0.4.0", path = "../brush-interactive", features = [
|
||||
"minimal",
|
||||
] }
|
||||
tokio = { version = "1.52.1", features = ["rt", "sync"] }
|
||||
|
||||
[target.'cfg(any(unix, windows))'.dependencies]
|
||||
brush-interactive = { version = "^0.4.0", path = "../brush-interactive", features = [
|
||||
"basic",
|
||||
"reedline",
|
||||
] }
|
||||
crossterm = "0.29.0"
|
||||
tokio = { version = "1.50.0", features = ["rt", "rt-multi-thread", "sync"] }
|
||||
|
||||
[target.wasm32-unknown-unknown.dependencies]
|
||||
getrandom = { version = "0.4.2", features = ["wasm_js"] }
|
||||
uuid = { version = "1.23.1", features = ["js"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.102"
|
||||
tempfile = "3.27.0"
|
||||
brush-parser = { version = "^0.4.0", path = "../brush-parser" }
|
||||
brush-test-harness = { path = "../brush-test-harness", features = ["insta"] }
|
||||
assert_cmd = "2.2.0"
|
||||
assert_fs = "1.1.3"
|
||||
colored = "3.1.1"
|
||||
criterion = { version = "0.8.2", features = ["async_tokio", "html_reports"] }
|
||||
descape = "3.0.0"
|
||||
diff = "0.1.13"
|
||||
expectrl = "0.8.0"
|
||||
glob = "0.3.3"
|
||||
indent = "0.1.1"
|
||||
indexmap = "2.13.0"
|
||||
junit-report = "0.9.0"
|
||||
os-release = "0.1.0"
|
||||
predicates = "3.1.4"
|
||||
pretty_assertions = { version = "1.4.1", features = ["unstable"] }
|
||||
regex = "1.12.3"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_yaml = "0.9.34"
|
||||
strip-ansi-escapes = "0.2.1"
|
||||
test-with = { version = "0.16.0", default-features = false, features = [
|
||||
"executable",
|
||||
] }
|
||||
version-compare = "0.2.1"
|
||||
walkdir = "2.5.0"
|
||||
which = "8.0.2"
|
||||
|
||||
[target.'cfg(unix)'.dev-dependencies]
|
||||
nix = { version = "0.31.2" }
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@@ -0,0 +1,70 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #333;
|
||||
color: white;
|
||||
}
|
||||
a {
|
||||
color: skyblue;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
font-family: sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.intro {
|
||||
text-align: center;
|
||||
}
|
||||
.licenses-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.license-used-by {
|
||||
margin-top: -10px;
|
||||
}
|
||||
.license-text {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="container">
|
||||
<div class="intro">
|
||||
<h1>Third Party Licenses</h1>
|
||||
<p>This page lists the licenses of the projects used in brush-shell.</p>
|
||||
</div>
|
||||
|
||||
<h2>Overview of licenses:</h2>
|
||||
<ul class="licenses-overview">
|
||||
{{#each overview}}
|
||||
<li><a href="#{{id}}">{{name}}</a> ({{count}})</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
<h2>All license text:</h2>
|
||||
<ul class="licenses-list">
|
||||
{{#each licenses}}
|
||||
<li class="license">
|
||||
<h3 id="{{id}}">{{name}}</h3>
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
{{#each used_by}}
|
||||
<li><a href="{{#if crate.repository}} {{crate.repository}} {{else}} https://crates.io/crates/{{crate.name}} {{/if}}">{{crate.name}} {{crate.version}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<pre class="license-text">{{text}}</pre>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
ignore-build-dependencies = true
|
||||
ignore-dev-dependencies = true
|
||||
no-clearly-defined = true
|
||||
|
||||
accepted = [
|
||||
"MIT",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSL-1.0",
|
||||
"ISC",
|
||||
"CDDL-1.0",
|
||||
"MPL-2.0",
|
||||
"NCSA",
|
||||
"Unicode-3.0",
|
||||
"WTFPL",
|
||||
"Zlib",
|
||||
]
|
||||
@@ -0,0 +1,174 @@
|
||||
//! Benchmarks for the brush-shell crate.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix {
|
||||
use brush_builtins::ShellBuilderExt;
|
||||
use brush_parser::SourceSpan;
|
||||
use criterion::Criterion;
|
||||
use std::hint::black_box;
|
||||
|
||||
async fn instantiate_shell() -> brush_core::Shell {
|
||||
brush_core::Shell::builder()
|
||||
.default_builtins(brush_builtins::BuiltinSet::BashMode)
|
||||
.build()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn instantiate_shell_with_init_scripts() -> brush_core::Shell {
|
||||
brush_core::Shell::builder()
|
||||
.interactive(true)
|
||||
.read_commands_from_stdin(true)
|
||||
.default_builtins(brush_builtins::BuiltinSet::BashMode)
|
||||
.build()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn run_one_command(shell: &mut brush_core::Shell, command: &str) {
|
||||
let _ = shell
|
||||
.run_string(
|
||||
command.to_owned(),
|
||||
&brush_core::SourceInfo::default(),
|
||||
&shell.default_exec_params(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn expand_string(shell: &mut brush_core::Shell, s: &str) {
|
||||
let params = shell.default_exec_params();
|
||||
let _ = shell.basic_expand_string(¶ms, s).await.unwrap();
|
||||
}
|
||||
|
||||
fn eval_arithmetic_expr(shell: &mut brush_core::Shell, expr: &str) {
|
||||
let parsed_expr = brush_parser::arithmetic::parse(expr).unwrap();
|
||||
let _ = shell.eval_arithmetic(&parsed_expr).unwrap();
|
||||
}
|
||||
|
||||
/// This function defines core shell benchmarks.
|
||||
pub(crate) fn criterion_benchmark(c: &mut Criterion) {
|
||||
// Construct a runtime for us to run async code on.
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Benchmark shell instantiation.
|
||||
c.bench_function("instantiate_shell", |b| {
|
||||
b.to_async(&rt).iter(|| black_box(instantiate_shell()));
|
||||
});
|
||||
c.bench_function("instantiate_shell_with_init_scripts", |b| {
|
||||
b.to_async(&rt)
|
||||
.iter(|| black_box(instantiate_shell_with_init_scripts()));
|
||||
});
|
||||
|
||||
// Benchmark: cloning a shell object.
|
||||
let shell = rt.block_on(instantiate_shell());
|
||||
c.bench_function("clone_shell_object", |b| {
|
||||
b.iter(|| black_box(shell.clone()));
|
||||
});
|
||||
|
||||
// Benchmark: parsing and evaluating an arithmetic expression..
|
||||
let shell = rt.block_on(instantiate_shell());
|
||||
c.bench_function("eval_arithmetic", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| shell.clone(),
|
||||
|s| eval_arithmetic_expr(s, "3 + 10 * 2"),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Benchmark: running the echo built-in command.
|
||||
let shell = rt.block_on(instantiate_shell());
|
||||
c.bench_function("run_echo_builtin_command", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| shell.clone(),
|
||||
|s| rt.block_on(run_one_command(s, "echo 'Hello, world!' >/dev/null")),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Benchmark: running an external command.
|
||||
// let shell = rt.block_on(instantiate_shell());
|
||||
// c.bench_function("run_one_external_command", |b| {
|
||||
// b.iter_batched_ref(
|
||||
// || shell.clone(),
|
||||
// |s| {
|
||||
// rt.block_on(run_one_command(
|
||||
// s,
|
||||
// "/usr/bin/echo 'Hello, world!' >/dev/null",
|
||||
// ));
|
||||
// },
|
||||
// criterion::BatchSize::SmallInput,
|
||||
// );
|
||||
// });
|
||||
|
||||
// Benchmark: word expansion.
|
||||
let shell = rt.block_on(instantiate_shell());
|
||||
c.bench_function("expand_one_string", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| shell.clone(),
|
||||
|s| rt.block_on(expand_string(s, "My version is ${BASH_VERSINFO[@]}")),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Benchmark: function invocation.
|
||||
let mut shell = rt.block_on(instantiate_shell());
|
||||
shell.define_func(
|
||||
String::from("testfunc"),
|
||||
brush_parser::ast::FunctionDefinition {
|
||||
fname: String::from("testfunc").into(),
|
||||
body: brush_parser::ast::FunctionBody(
|
||||
brush_parser::ast::CompoundCommand::BraceGroup(
|
||||
brush_parser::ast::BraceGroupCommand {
|
||||
list: brush_parser::ast::CompoundList(vec![]),
|
||||
loc: SourceSpan::default(),
|
||||
},
|
||||
),
|
||||
None,
|
||||
),
|
||||
},
|
||||
&brush_core::SourceInfo::default(),
|
||||
);
|
||||
c.bench_function("function_call", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| shell.clone(),
|
||||
|s| {
|
||||
rt.block_on(run_one_command(s, "testfunc"));
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Benchmark: for loop.
|
||||
let shell = rt.block_on(instantiate_shell());
|
||||
c.bench_function("for_loop", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| shell.clone(),
|
||||
|s| {
|
||||
rt.block_on(run_one_command(s, "for ((i = 0; i < 10; i++)); do :; done"));
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
criterion::criterion_group! {
|
||||
name = benches;
|
||||
config = criterion::Criterion::default()
|
||||
.measurement_time(std::time::Duration::from_secs(10));
|
||||
targets = unix::criterion_benchmark
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
criterion::criterion_main!(benches);
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn main() {}
|
||||
@@ -0,0 +1,304 @@
|
||||
//! Types for brush command-line parsing.
|
||||
|
||||
use clap::{Parser, builder::styling};
|
||||
use std::io::IsTerminal;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{events, productinfo};
|
||||
|
||||
const SHORT_DESCRIPTION: &str = "Bo[u]rn[e] RUsty SHell 🦀 (https://brush.sh)";
|
||||
|
||||
const LONG_DESCRIPTION: &str = r"brush is a bash-compatible, Rust-implemented, POSIX-style shell.
|
||||
|
||||
brush is distributed under the terms of the MIT license. If you encounter any issues or discrepancies in behavior from bash, please report them at https://github.com/reubeno/brush.
|
||||
|
||||
For more information, visit https://brush.sh.";
|
||||
|
||||
// nash: rebranded usage/version strings for the Nucleic agent shell fork.
|
||||
const USAGE: &str = color_print::cstr!(
|
||||
"<bold>nash</bold> <italics>[OPTIONS]</italics>... <italics>[SCRIPT_PATH [SCRIPT_ARGS]...]</italics>"
|
||||
);
|
||||
|
||||
const VERSION: &str = const_format::concatcp!(
|
||||
productinfo::PRODUCT_VERSION,
|
||||
" (Nucleic agent shell, brush fork; ",
|
||||
productinfo::PRODUCT_GIT_VERSION,
|
||||
")"
|
||||
);
|
||||
|
||||
const HEADING_STANDARD_OPTIONS: &str = "Standard shell options";
|
||||
|
||||
const HEADING_CONFIG_OPTIONS: &str = "Configuration options";
|
||||
|
||||
const HEADING_UI_OPTIONS: &str = "User interface options";
|
||||
|
||||
const HEADING_EXPERIMENTAL_OPTIONS: &str = "*Experimental* options (unstable)";
|
||||
|
||||
/// Identifies the input backend to use for the shell.
|
||||
#[derive(Clone, Copy, clap::ValueEnum)]
|
||||
pub enum InputBackendType {
|
||||
/// Richest input backend, based on reedline.
|
||||
Reedline,
|
||||
/// Basic input backend that provides minimal completion support for testing.
|
||||
Basic,
|
||||
/// Most minimal input backend.
|
||||
Minimal,
|
||||
}
|
||||
|
||||
/// Parsed command-line arguments for the brush shell.
|
||||
#[derive(Clone, Parser)]
|
||||
#[clap(name = productinfo::PRODUCT_NAME,
|
||||
version = VERSION,
|
||||
about = SHORT_DESCRIPTION,
|
||||
long_about = LONG_DESCRIPTION,
|
||||
author,
|
||||
override_usage = USAGE,
|
||||
disable_help_flag = true,
|
||||
disable_version_flag = true,
|
||||
styles = brush_help_styles())]
|
||||
pub struct CommandLineArgs {
|
||||
/// Display usage information.
|
||||
#[clap(long = "help", action = clap::ArgAction::HelpShort)]
|
||||
pub help: Option<bool>,
|
||||
|
||||
/// Display shell version.
|
||||
#[clap(long = "version", action = clap::ArgAction::Version)]
|
||||
pub version: Option<bool>,
|
||||
|
||||
/// Path to TOML-based `brush` config file (overrides default location).
|
||||
#[clap(long = "config", value_name = "FILE", help_heading = HEADING_CONFIG_OPTIONS)]
|
||||
pub config_file: Option<PathBuf>,
|
||||
|
||||
/// Disable loading of TOML-based `brush` config file.
|
||||
#[clap(long = "no-config", help_heading = HEADING_CONFIG_OPTIONS)]
|
||||
pub no_config: bool,
|
||||
|
||||
/// Enable `noclobber` shell option.
|
||||
#[arg(short = 'C', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub disallow_overwriting_regular_files_via_output_redirection: bool,
|
||||
|
||||
/// Execute the provided command and then exit.
|
||||
#[arg(short = 'c', value_name = "COMMAND", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub command: Option<String>,
|
||||
|
||||
/// Enable error-on-exit behavior.
|
||||
#[clap(short = 'e', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub exit_on_nonzero_command_exit: bool,
|
||||
|
||||
/// Disable pathname expansion (also known as filename globbing).
|
||||
#[clap(short = 'f', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub disable_pathname_expansion: bool,
|
||||
|
||||
/// Run in interactive mode.
|
||||
#[clap(short = 'i', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub interactive: bool,
|
||||
|
||||
/// Inherit the specified file descriptors injected by the parent process.
|
||||
#[clap(long = "inherit-fd", value_name = "FD", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub inherited_fds: Vec<i32>,
|
||||
|
||||
/// Make shell act as if it had been invoked as a login shell.
|
||||
#[clap(short = 'l', long = "login", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub login: bool,
|
||||
|
||||
/// Do not execute commands.
|
||||
#[clap(short = 'n', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub do_not_execute_commands: bool,
|
||||
|
||||
/// Don't use readline for input.
|
||||
#[clap(long = "noediting", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub no_editing: bool,
|
||||
|
||||
/// Don't process any profile/login files (`/etc/profile`, `~/.bash_profile`, `~/.bash_login`,
|
||||
/// `~/.profile`).
|
||||
#[clap(long = "noprofile", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub no_profile: bool,
|
||||
|
||||
/// Don't process "rc" files if the shell is interactive (e.g., `~/.bashrc`, `~/.brushrc`).
|
||||
#[clap(long = "norc", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub no_rc: bool,
|
||||
|
||||
/// Don't inherit environment variables from the calling process.
|
||||
#[clap(long = "noenv", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub do_not_inherit_env: bool,
|
||||
|
||||
/// Enable option (`set -o` option).
|
||||
#[clap(short = 'o', value_name = "OPTION", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub enabled_options: Vec<String>,
|
||||
|
||||
/// Disable option (`set -o` option).
|
||||
#[clap(long = "+o", value_name = "OPTION", hide = true, help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub disabled_options: Vec<String>,
|
||||
|
||||
/// Enable `shopt` option.
|
||||
#[clap(short = 'O', value_name = "SHOPT_OPTION", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub enabled_shopt_options: Vec<String>,
|
||||
|
||||
/// Disable `shopt` option.
|
||||
#[clap(long = "+O", value_name = "SHOPT_OPTION", hide = true, help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub disabled_shopt_options: Vec<String>,
|
||||
|
||||
/// Disable non-POSIX extensions.
|
||||
#[clap(long = "posix", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub posix: bool,
|
||||
|
||||
/// Path to the rc file to load in interactive shells (instead of `bash.bashrc` and
|
||||
/// `~/.bashrc`).
|
||||
#[clap(long = "rcfile", alias = "init-file", value_name = "FILE", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub rc_file: Option<PathBuf>,
|
||||
|
||||
/// Read commands from standard input.
|
||||
#[clap(short = 's', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub read_commands_from_stdin: bool,
|
||||
|
||||
/// Run in `sh` compatibility mode, as if run as `/bin/sh`.
|
||||
#[clap(long = "sh")]
|
||||
pub sh_mode: bool,
|
||||
|
||||
/// Run only one command and then exit.
|
||||
#[clap(short = 't', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub exit_after_one_command: bool,
|
||||
|
||||
/// Treat expansion of an unset variable as an error.
|
||||
#[clap(short = 'u', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub treat_unset_variables_as_error: bool,
|
||||
|
||||
/// Print input when it's processed.
|
||||
#[clap(short = 'v', long = "verbose", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub verbose: bool,
|
||||
|
||||
/// Print commands as they execute.
|
||||
#[clap(short = 'x', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub print_commands_and_arguments: bool,
|
||||
|
||||
/// Enable xtrace and configure for the given output file.
|
||||
#[clap(long = "xtrace-file", value_name = "FILE", help_heading = HEADING_UI_OPTIONS)]
|
||||
pub xtrace_file_path: Option<PathBuf>,
|
||||
|
||||
/// Disable bracketed paste.
|
||||
#[clap(long = "disable-bracketed-paste", help_heading = HEADING_UI_OPTIONS)]
|
||||
pub disable_bracketed_paste: bool,
|
||||
|
||||
/// Disable colorized output.
|
||||
#[clap(long = "disable-color", help_heading = HEADING_UI_OPTIONS)]
|
||||
pub disable_color: bool,
|
||||
|
||||
/// Enable syntax highlighting in input.
|
||||
#[clap(long = "enable-highlighting", help_heading = HEADING_UI_OPTIONS, default_value_t = crate::entry::DEFAULT_ENABLE_HIGHLIGHTING)]
|
||||
pub enable_highlighting: bool,
|
||||
|
||||
/// Enable experimental parser (not ready for use).
|
||||
#[cfg(feature = "experimental-parser")]
|
||||
#[clap(long = "experimental-parser", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
|
||||
pub experimental_parser: bool,
|
||||
|
||||
/// Enable terminal integration (**experimental**).
|
||||
#[clap(long = "enable-terminal-integration", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
|
||||
pub terminal_shell_integration: bool,
|
||||
|
||||
/// Enable zsh-style preexec/precmd hooks (**experimental**).
|
||||
#[clap(long = "enable-zsh-hooks", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
|
||||
pub zsh_style_hooks: bool,
|
||||
|
||||
/// Input backend.
|
||||
#[clap(long = "input-backend", value_name = "BACKEND", help_heading = HEADING_UI_OPTIONS)]
|
||||
pub input_backend: Option<InputBackendType>,
|
||||
|
||||
/// Load state from the given file; the saved state should be in JSON format
|
||||
/// and overrides any non-UI command-line options provided.
|
||||
#[cfg(feature = "experimental-load")]
|
||||
#[clap(long = "load", value_name = "FILE", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
|
||||
pub load_file: Option<PathBuf>,
|
||||
|
||||
/// Enable debug logging for classes of tracing events.
|
||||
#[clap(long = "debug", alias = "log-enable", value_name = "EVENT", help_heading = HEADING_UI_OPTIONS)]
|
||||
pub enabled_debug_events: Vec<events::TraceEvent>,
|
||||
|
||||
/// Disable logging for classes of tracing events (takes same event types as `--debug`).
|
||||
#[clap(
|
||||
long = "disable-event",
|
||||
alias = "log-disable",
|
||||
value_name = "EVENT",
|
||||
hide_possible_values = true,
|
||||
help_heading = HEADING_UI_OPTIONS
|
||||
)]
|
||||
pub disabled_events: Vec<events::TraceEvent>,
|
||||
|
||||
/// Path and arguments for script to execute (optional).
|
||||
#[clap(
|
||||
trailing_var_arg = true,
|
||||
allow_hyphen_values = false,
|
||||
value_name = "SCRIPT_PATH [SCRIPT_ARGS]..."
|
||||
)]
|
||||
pub script_args: Vec<String>,
|
||||
}
|
||||
|
||||
impl CommandLineArgs {
|
||||
/// Returns a `CommandLineArgs` with all clap-defined default values.
|
||||
///
|
||||
/// This is useful for detecting which CLI arguments were explicitly provided
|
||||
/// vs. which retained their default values (e.g., for config file merging).
|
||||
#[must_use]
|
||||
#[allow(
|
||||
clippy::missing_panics_doc,
|
||||
reason = "parsing defaults should not panic"
|
||||
)]
|
||||
pub fn default_values() -> Self {
|
||||
use clap::Parser;
|
||||
// Parse with just the program name to get all defaults.
|
||||
// This won't fail because all arguments have defaults or are optional.
|
||||
#[allow(clippy::expect_used)]
|
||||
Self::try_parse_from(["brush"]).expect("parsing defaults should never fail")
|
||||
}
|
||||
|
||||
/// Returns whether or not the arguments indicate that the shell should run in interactive mode.
|
||||
pub fn is_interactive(&self) -> bool {
|
||||
// If -i is provided, then that overrides any further consideration; it forces
|
||||
// interactive mode.
|
||||
if self.interactive {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If -c or non-option arguments are provided, then we're not in interactive mode.
|
||||
if self.command.is_some() || !self.script_args.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If *either* stdin or stderr is not a terminal, then we're not in interactive mode.
|
||||
if !std::io::stdin().is_terminal() || !std::io::stderr().is_terminal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// In all other cases, we assume interactive mode.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns clap styling to be used for command-line help.
|
||||
#[doc(hidden)]
|
||||
fn brush_help_styles() -> clap::builder::Styles {
|
||||
styling::Styles::styled()
|
||||
.header(
|
||||
styling::AnsiColor::Yellow.on_default()
|
||||
| styling::Effects::BOLD
|
||||
| styling::Effects::UNDERLINE,
|
||||
)
|
||||
.usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
|
||||
.literal(styling::AnsiColor::Magenta.on_default() | styling::Effects::BOLD)
|
||||
.placeholder(styling::AnsiColor::Cyan.on_default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_values() {
|
||||
let args = CommandLineArgs::default_values();
|
||||
// Verify some basic defaults
|
||||
assert!(!args.interactive);
|
||||
assert!(!args.login);
|
||||
assert!(args.command.is_none());
|
||||
assert!(args.script_args.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
use brush_core::{ExecutionResult, sys};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::io::Write;
|
||||
|
||||
use crate::events;
|
||||
|
||||
/// Extension trait for adding brush-specific built-in commands to a shell builder.
|
||||
pub(crate) trait ShellBuilderBrushBuiltinExt {
|
||||
/// Add brush-specific builtins to a shell being built.
|
||||
#[must_use]
|
||||
fn brush_builtins(self) -> Self;
|
||||
}
|
||||
|
||||
impl<SE: brush_core::extensions::ShellExtensions, S: brush_core::ShellBuilderState>
|
||||
ShellBuilderBrushBuiltinExt for brush_core::ShellBuilder<SE, S>
|
||||
{
|
||||
fn brush_builtins(self) -> Self {
|
||||
// For compatibility with previous releases, we register the command under both
|
||||
// `brushctl` and `brushinfo` names. It will behave identically across the two.
|
||||
self.builtin(
|
||||
"brushctl",
|
||||
brush_core::builtins::builtin::<BrushCtlCommand, SE>(),
|
||||
)
|
||||
.builtin(
|
||||
"brushinfo",
|
||||
brush_core::builtins::builtin::<BrushCtlCommand, SE>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure the running brush shell.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct BrushCtlCommand {
|
||||
#[clap(subcommand)]
|
||||
command_group: CommandGroup,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum CommandGroup {
|
||||
#[clap(subcommand)]
|
||||
Complete(CompleteCommand),
|
||||
#[clap(subcommand)]
|
||||
Call(CallCommand),
|
||||
#[clap(subcommand)]
|
||||
Events(EventsCommand),
|
||||
#[clap(subcommand)]
|
||||
Process(ProcessCommand),
|
||||
}
|
||||
|
||||
/// Commands for inspecting call state.
|
||||
#[derive(Subcommand)]
|
||||
enum CallCommand {
|
||||
/// Display the current call stack.
|
||||
#[clap(name = "stack")]
|
||||
ShowCallStack {
|
||||
/// Whether to show more details.
|
||||
#[clap(short = 'd', long = "detailed")]
|
||||
detailed: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Commands for generating completions.
|
||||
#[derive(Subcommand)]
|
||||
enum CompleteCommand {
|
||||
/// Generate completions for an input line.
|
||||
#[clap(name = "line")]
|
||||
Line {
|
||||
/// The 0-indexed cursor position for generation.
|
||||
#[arg(long = "cursor", short = 'c')]
|
||||
cursor_index: Option<usize>,
|
||||
|
||||
/// The input line to generate completions for.
|
||||
line: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Commands for configuring tracing events.
|
||||
#[derive(Subcommand)]
|
||||
enum EventsCommand {
|
||||
/// Display status of enabled events.
|
||||
Status,
|
||||
|
||||
/// Enable event.
|
||||
Enable {
|
||||
/// Event to enable.
|
||||
event: events::TraceEvent,
|
||||
},
|
||||
|
||||
/// Disable event.
|
||||
Disable {
|
||||
/// Event to disable.
|
||||
event: events::TraceEvent,
|
||||
},
|
||||
}
|
||||
|
||||
/// Commands for inspecting process state.
|
||||
#[expect(clippy::enum_variant_names)]
|
||||
#[derive(Subcommand)]
|
||||
enum ProcessCommand {
|
||||
/// Display process ID.
|
||||
#[clap(name = "pid")]
|
||||
ShowProcessId,
|
||||
/// Display process group ID.
|
||||
#[clap(name = "pgid")]
|
||||
ShowProcessGroupId,
|
||||
/// Display foreground process ID.
|
||||
#[clap(name = "fgpid")]
|
||||
ShowForegroundProcessId,
|
||||
/// Display parent process ID.
|
||||
#[clap(name = "ppid")]
|
||||
ShowParentProcessId,
|
||||
}
|
||||
|
||||
impl brush_core::builtins::Command for BrushCtlCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
match &self.command_group {
|
||||
CommandGroup::Call(call) => call.execute(&context),
|
||||
CommandGroup::Complete(complete) => complete.execute(&mut context).await,
|
||||
CommandGroup::Events(events) => events.execute(&context),
|
||||
CommandGroup::Process(process) => process.execute(&context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CallCommand {
|
||||
fn execute(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<brush_core::ExecutionResult, brush_core::Error> {
|
||||
match self {
|
||||
Self::ShowCallStack { detailed } => {
|
||||
let stack = context.shell.call_stack();
|
||||
let format_options = brush_core::callstack::FormatOptions {
|
||||
show_args: *detailed,
|
||||
show_entry_points: *detailed,
|
||||
};
|
||||
|
||||
write!(context.stdout(), "{}", stack.format(&format_options))?;
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompleteCommand {
|
||||
async fn execute(
|
||||
&self,
|
||||
context: &mut brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<brush_core::ExecutionResult, brush_core::Error> {
|
||||
match self {
|
||||
Self::Line { cursor_index, line } => {
|
||||
let completions = context
|
||||
.shell
|
||||
.complete(line, cursor_index.unwrap_or(line.len()))
|
||||
.await?;
|
||||
for candidate in completions.candidates {
|
||||
writeln!(context.stdout(), "{candidate}")?;
|
||||
}
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventsCommand {
|
||||
fn execute(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<brush_core::ExecutionResult, brush_core::Error> {
|
||||
let event_config = crate::entry::get_event_config();
|
||||
|
||||
let mut event_config = event_config.try_lock().map_err(|_| {
|
||||
brush_core::Error::from(brush_core::ErrorKind::Unimplemented(
|
||||
"Failed to acquire lock on event configuration",
|
||||
))
|
||||
})?;
|
||||
|
||||
if let Some(event_config) = event_config.as_mut() {
|
||||
match self {
|
||||
Self::Status => {
|
||||
let enabled_events = event_config.get_enabled_events();
|
||||
for event in enabled_events {
|
||||
writeln!(context.stdout(), "{event}")?;
|
||||
}
|
||||
}
|
||||
Self::Enable { event } => event_config.enable(*event)?,
|
||||
Self::Disable { event } => event_config.disable(*event)?,
|
||||
}
|
||||
|
||||
Ok(brush_core::ExecutionResult::success())
|
||||
} else {
|
||||
Err(brush_core::ErrorKind::Unimplemented("event configuration not initialized").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessCommand {
|
||||
fn execute(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<brush_core::ExecutionResult, brush_core::Error> {
|
||||
match self {
|
||||
Self::ShowProcessId => {
|
||||
writeln!(context.stdout(), "{}", std::process::id())?;
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
Self::ShowProcessGroupId => {
|
||||
if let Some(pgid) = sys::terminal::get_process_group_id() {
|
||||
writeln!(context.stdout(), "{pgid}")?;
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
writeln!(context.stderr(), "failed to get process group ID")?;
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
}
|
||||
Self::ShowForegroundProcessId => {
|
||||
if let Some(pid) = sys::terminal::get_foreground_pid() {
|
||||
writeln!(context.stdout(), "{pid}")?;
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
writeln!(context.stderr(), "failed to get foreground process ID")?;
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
}
|
||||
Self::ShowParentProcessId => {
|
||||
if let Some(pid) = sys::terminal::get_parent_process_id() {
|
||||
writeln!(context.stdout(), "{pid}")?;
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
writeln!(context.stderr(), "failed to get parent process ID")?;
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
//! Bundled commands: utilities that ship inside the brush binary.
|
||||
//!
|
||||
//! Utilities are shipped busybox-style (one binary, many names) but execute
|
||||
//! as a subprocess of brush so that shell redirections, pipes, and
|
||||
//! process-group state are honored by code that reads/writes the host
|
||||
//! process's standard fds (e.g., uutils crates).
|
||||
//!
|
||||
//! ## Protocol
|
||||
//!
|
||||
//! The brush binary recognizes a hidden first-position argument
|
||||
//! [`DISPATCH_FLAG`] followed by `<NAME> [ARGS...]`. When present, brush
|
||||
//! dispatches early in `main()` to the registered function for `NAME`, before
|
||||
//! any shell state is built, and exits with the function's return code. The
|
||||
//! dispatched function has the same signature as `uutils`' `uumain`:
|
||||
//! `fn(Vec<OsString>) -> i32`, with the bundled name as `argv[0]`.
|
||||
//!
|
||||
//! ## Shell integration
|
||||
//!
|
||||
//! For every entry in the registry, [`register_shims`] installs a brush
|
||||
//! builtin (using `register_builtin_if_unset`, so brush's own builtins always
|
||||
//! win on conflict). The builtin's execution path uses brush-core's existing
|
||||
//! external-command machinery to spawn `current_exe() <DISPATCH_FLAG> <name>
|
||||
//! <args...>`, inheriting the shell's redirection state for free.
|
||||
//!
|
||||
//! The mechanism is generic — the registry is just `name → fn pointer`. The
|
||||
//! `experimental-bundled-coreutils` feature populates it with uutils, but
|
||||
//! anything matching the signature can be registered.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use brush_core::ExecutionExitCode;
|
||||
use brush_core::builtins::{BoxFuture, ContentOptions, ContentType, Registration};
|
||||
use brush_core::commands::{self, CommandArg, ExecutionContext};
|
||||
use brush_core::extensions::ShellExtensions;
|
||||
|
||||
/// The leading flag that signals a bundled-command dispatch.
|
||||
///
|
||||
/// Deliberately obscure so that it's unlikely to collide with future
|
||||
/// first-class shell flags or with scripts that happen to contain the
|
||||
/// literal token.
|
||||
pub const DISPATCH_FLAG: &str = "--invoke-bundled";
|
||||
|
||||
/// Signature of a bundled command's entry point — matches `uu_*::uumain`.
|
||||
pub type BundledFn = fn(args: Vec<OsString>) -> i32;
|
||||
|
||||
/// Process-wide registry. Set once at startup, read on each shim invocation
|
||||
/// (and during bundled-dispatch fast path).
|
||||
static REGISTRY: OnceLock<HashMap<String, BundledFn>> = OnceLock::new();
|
||||
|
||||
/// Cached path to the running brush executable. Populated lazily on first
|
||||
/// shim invocation; left as `Err`-equivalent if `current_exe()` fails.
|
||||
static SELF_EXE: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||
|
||||
/// Installs the bundled-command registry. Idempotent: only the first call
|
||||
/// takes effect.
|
||||
#[allow(
|
||||
clippy::implicit_hasher,
|
||||
reason = "registry uses the default hasher; callers build with HashMap::new()"
|
||||
)]
|
||||
pub fn install(commands: HashMap<String, BundledFn>) {
|
||||
let _ = REGISTRY.set(commands);
|
||||
}
|
||||
|
||||
/// Installs the registry from all compiled-in providers.
|
||||
///
|
||||
/// Providers are controlled by Cargo features. Binaries should call this
|
||||
/// once, before [`maybe_dispatch`], so both the dispatch fast path and the
|
||||
/// shell's shim builtins see a populated registry.
|
||||
pub fn install_default_providers() {
|
||||
#[allow(unused_mut)]
|
||||
let mut commands: HashMap<String, BundledFn> = HashMap::new();
|
||||
|
||||
#[cfg(feature = "experimental-bundled-coreutils")]
|
||||
commands.extend(brush_coreutils_builtins::bundled_commands());
|
||||
|
||||
install(commands);
|
||||
}
|
||||
|
||||
/// Returns the registered bundled commands, if [`install`] was called.
|
||||
#[must_use]
|
||||
pub fn registry() -> Option<&'static HashMap<String, BundledFn>> {
|
||||
REGISTRY.get()
|
||||
}
|
||||
|
||||
/// Runs the bundled-command fast path if the process was invoked for it.
|
||||
///
|
||||
/// If the process was invoked as `brush <DISPATCH_FLAG> <NAME> [ARGS...]`
|
||||
/// (with `<DISPATCH_FLAG>` as the very first argument after `argv[0]`), runs
|
||||
/// the registered function and returns its exit code as `Some(code)`. The
|
||||
/// caller is responsible for exiting the process with that code —
|
||||
/// centralizing the exit call in the binary's `main()` keeps destructors /
|
||||
/// panic hooks / tracing guards in the loop.
|
||||
///
|
||||
/// Returns `None` when the process was not invoked as a bundled dispatch, so
|
||||
/// normal shell startup can proceed.
|
||||
///
|
||||
/// The dispatch flag is only recognized in the leading position so that
|
||||
/// ordinary scripts and command lines containing the literal token elsewhere
|
||||
/// are not affected.
|
||||
#[must_use]
|
||||
pub fn maybe_dispatch() -> Option<i32> {
|
||||
let mut raw = std::env::args_os();
|
||||
let _argv0 = raw.next();
|
||||
let first = raw.next()?;
|
||||
if first != DISPATCH_FLAG {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Everything after `DISPATCH_FLAG` belongs to the bundled command. The
|
||||
// first such argument is the command name; subsequent arguments form its
|
||||
// argv (with the name itself supplied as argv[0] to match the convention
|
||||
// `uutils` and most CLI tools expect).
|
||||
let rest: Vec<OsString> = raw.collect();
|
||||
let Some((name, args)) = rest.split_first() else {
|
||||
eprintln!("brush: {DISPATCH_FLAG} requires a command name");
|
||||
return Some(exit_code(ExecutionExitCode::InvalidUsage));
|
||||
};
|
||||
|
||||
// The registry is keyed by UTF-8 `String`, so a non-UTF-8 name can never
|
||||
// match. Reject up front rather than allocating a lossy-substituted
|
||||
// lookup key that could accidentally collide with a real registration.
|
||||
let Some(name_str) = name.to_str() else {
|
||||
eprintln!("brush: unknown bundled command: {}", name.to_string_lossy());
|
||||
return Some(exit_code(ExecutionExitCode::NotFound));
|
||||
};
|
||||
|
||||
let Some(func) = REGISTRY.get().and_then(|r| r.get(name_str)) else {
|
||||
eprintln!("brush: unknown bundled command: {name_str}");
|
||||
return Some(exit_code(ExecutionExitCode::NotFound));
|
||||
};
|
||||
|
||||
let mut argv: Vec<OsString> = Vec::with_capacity(1 + args.len());
|
||||
argv.push(name.clone());
|
||||
argv.extend(args.iter().cloned());
|
||||
|
||||
Some(func(argv))
|
||||
}
|
||||
|
||||
fn exit_code(code: ExecutionExitCode) -> i32 {
|
||||
u8::from(code).into()
|
||||
}
|
||||
|
||||
/// Returns the path to the running brush executable (cached).
|
||||
fn self_exe() -> Option<&'static PathBuf> {
|
||||
SELF_EXE
|
||||
.get_or_init(|| std::env::current_exe().ok())
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Help/usage content provider for the shim builtin. brush calls this for
|
||||
/// `help <name>`, `type <name>`, etc.
|
||||
#[allow(
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::unnecessary_wraps,
|
||||
reason = "signature dictated by brush_core::builtins::CommandContentFunc"
|
||||
)]
|
||||
fn shim_content(
|
||||
name: &str,
|
||||
content_type: ContentType,
|
||||
_options: &ContentOptions,
|
||||
) -> Result<String, brush_core::Error> {
|
||||
match content_type {
|
||||
ContentType::ShortDescription => Ok(format!("{name} - bundled command")),
|
||||
ContentType::DetailedHelp => Ok(format!(
|
||||
"{name} - bundled command (executes via `brush {DISPATCH_FLAG} {name}`)\n"
|
||||
)),
|
||||
// A bundled command never contributes its own short-usage or man page
|
||||
// through this path; detailed help comes from the bundled utility
|
||||
// itself (`brush <DISPATCH_FLAG> <name> --help` or equivalent).
|
||||
ContentType::ShortUsage | ContentType::ManPage => Ok(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builtin execute function shared by all bundled commands. Looks up the
|
||||
/// invoked name from `context.command_name` and re-executes the running
|
||||
/// brush binary as `brush <DISPATCH_FLAG> <name> <args>`.
|
||||
///
|
||||
/// Reuses the same entry point the `command` builtin uses (see
|
||||
/// `brush-builtins/src/command.rs`): constructs a [`commands::SimpleCommand`]
|
||||
/// whose `command_name` is the absolute brush exe path. Because that contains
|
||||
/// a path separator, `SimpleCommand::execute` routes directly to the
|
||||
/// external-execution path, bypassing the builtin/function lookup that would
|
||||
/// otherwise re-enter this very shim.
|
||||
///
|
||||
/// `use_functions = false` is defensive: even though the path-separator
|
||||
/// branch already skips function dispatch, we don't want a hypothetical
|
||||
/// refactor of `SimpleCommand` to silently break us.
|
||||
//
|
||||
// TODO(bundled): Process-group propagation.
|
||||
// The shim leaves `SimpleCommand::process_group_id` as `None`, so when a
|
||||
// bundled command appears in a pipeline it doesn't join the pipeline's
|
||||
// pgid — job control and pipeline-wide signal delivery misbehave.
|
||||
// `ExecutionContext` doesn't currently carry the dispatcher's pgid, so
|
||||
// fixing this requires plumbing the pgid through the builtin dispatch
|
||||
// boundary (likely as a field on `ExecutionParameters` or a new
|
||||
// `ExecutionContext` accessor).
|
||||
//
|
||||
// TODO(bundled): Pipeline serialization.
|
||||
// The builtin contract returns an `ExecutionResult` (a completed command),
|
||||
// not an `ExecutionSpawnResult` (a spawn handle), so this function has to
|
||||
// `.await` the child to completion before returning. That's fine for a
|
||||
// standalone bundled command or for the tail of a pipeline, but for a
|
||||
// bundled stage in the middle of `a | b | c` it means stage N only
|
||||
// "starts" (from brush's perspective) after its child has fully exited —
|
||||
// downstream stages get no parallelism with it. Fixing this means
|
||||
// bypassing the builtin API for bundled dispatch: either detect the shim
|
||||
// inside `SimpleCommand::execute`'s dispatch table and return an
|
||||
// `ExecutionSpawnResult::StartedProcess` directly (same shape as external
|
||||
// dispatch), or generalize the builtin API so a builtin can return a
|
||||
// spawn handle instead of a finished result.
|
||||
fn shim_execute<SE: ShellExtensions>(
|
||||
context: ExecutionContext<'_, SE>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> BoxFuture<'_, Result<brush_core::ExecutionResult, brush_core::Error>> {
|
||||
Box::pin(async move {
|
||||
let exe_path = if let Some(p) = self_exe() {
|
||||
p.to_string_lossy().into_owned()
|
||||
} else {
|
||||
let _ = writeln!(
|
||||
context.stderr(),
|
||||
"brush: cannot determine path to running executable"
|
||||
);
|
||||
return Ok(ExecutionExitCode::CannotExecute.into());
|
||||
};
|
||||
|
||||
// Build the argv for the spawned brush. `SimpleCommand::args[0]` is
|
||||
// dropped by the external-execution path (argv[0] of the spawned
|
||||
// process comes from `cmd.argv0` below), so a placeholder suffices;
|
||||
// args[1..] become the spawned process's argv[1..]. The caller's
|
||||
// `args[0]` is the bundled name by builtin-dispatch convention — we
|
||||
// replace it with an explicit `<name>` after `DISPATCH_FLAG` so the
|
||||
// child's dispatcher sees it in a fixed slot.
|
||||
let bundled_name = context.command_name.clone();
|
||||
let mut child_args: Vec<CommandArg> = Vec::with_capacity(args.len() + 2);
|
||||
child_args.push(CommandArg::String(String::new())); // args[0], dropped
|
||||
child_args.push(CommandArg::String(DISPATCH_FLAG.into()));
|
||||
child_args.push(CommandArg::String(bundled_name.clone()));
|
||||
child_args.extend(args.into_iter().skip(1));
|
||||
|
||||
let mut cmd = commands::SimpleCommand::new(
|
||||
commands::ShellForCommand::ParentShell(context.shell),
|
||||
context.params,
|
||||
exe_path,
|
||||
child_args,
|
||||
);
|
||||
cmd.use_functions = false;
|
||||
// Override the spawned process's argv[0] so tools that report errors
|
||||
// via their own argv[0] (uutils' `uucore::util_name()` reads
|
||||
// `std::env::args_os()[0]` into a LazyLock at first use) render as
|
||||
// `<name>:` rather than `brush:`. Without this the child sees the
|
||||
// brush exe path as argv[0] and misattributes errors.
|
||||
cmd.argv0 = Some(bundled_name);
|
||||
|
||||
let spawn_result = cmd.execute().await?;
|
||||
let wait_result = spawn_result.wait().await?;
|
||||
Ok(wait_result.into())
|
||||
})
|
||||
}
|
||||
|
||||
/// Constructs a [`Registration`] for the bundled-shim builtin. The same
|
||||
/// registration value can be reused for every bundled name; per-name
|
||||
/// dispatch happens via `context.command_name` at execution time.
|
||||
fn shim_registration<SE: ShellExtensions>() -> Registration<SE> {
|
||||
Registration {
|
||||
execute_func: shim_execute::<SE>,
|
||||
content_func: shim_content,
|
||||
disabled: false,
|
||||
special_builtin: false,
|
||||
declaration_builtin: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a shim builtin for every name in the installed bundled-command
|
||||
/// registry.
|
||||
///
|
||||
/// Uses `register_builtin_if_unset` so brush's own builtins (echo, printf,
|
||||
/// true, false, etc.) win on conflict.
|
||||
pub fn register_shims<SE: ShellExtensions>(shell: &mut brush_core::Shell<SE>) {
|
||||
let Some(registry) = REGISTRY.get() else {
|
||||
return;
|
||||
};
|
||||
for name in registry.keys() {
|
||||
shell.register_builtin_if_unset(name.clone(), shim_registration::<SE>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
//! Configuration file support for the brush shell.
|
||||
//!
|
||||
//! This module provides TOML-based configuration file loading with the following features:
|
||||
//! - Forward-compatible: unknown fields are ignored
|
||||
//! - Graceful degradation: parse errors are logged but don't prevent shell startup
|
||||
//! - Layered configuration: defaults < config file < command-line arguments
|
||||
|
||||
use brush_interactive::UIOptions;
|
||||
use etcetera::BaseStrategy;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::args::CommandLineArgs;
|
||||
|
||||
/// Root configuration structure for the brush shell.
|
||||
///
|
||||
/// All fields are optional to support forward compatibility and partial configuration.
|
||||
/// Unknown fields in the TOML file are silently ignored.
|
||||
#[derive(Debug, Default, Clone, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
/// User interface configuration options.
|
||||
pub ui: UiConfig,
|
||||
|
||||
/// Experimental features configuration.
|
||||
pub experimental: ExperimentalConfig,
|
||||
}
|
||||
|
||||
/// User interface configuration options.
|
||||
#[derive(Debug, Default, Clone, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(default)]
|
||||
pub struct UiConfig {
|
||||
/// Enable syntax highlighting in the input line.
|
||||
#[serde(rename = "syntax-highlighting")]
|
||||
pub syntax_highlighting: Option<bool>,
|
||||
}
|
||||
|
||||
/// Experimental features configuration.
|
||||
///
|
||||
/// These options control unstable features that may change or be removed in future versions.
|
||||
#[derive(Debug, Default, Clone, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(default)]
|
||||
pub struct ExperimentalConfig {
|
||||
/// Enable zsh-style preexec/precmd hooks.
|
||||
#[serde(rename = "zsh-hooks")]
|
||||
pub zsh_hooks: Option<bool>,
|
||||
|
||||
/// Enable terminal shell integration.
|
||||
#[serde(rename = "terminal-shell-integration")]
|
||||
pub terminal_shell_integration: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Converts the configuration to [`UIOptions`], merging with CLI arguments.
|
||||
///
|
||||
/// Settings are applied with the following priority (highest to lowest):
|
||||
/// 1. CLI arguments (if explicitly set, i.e., different from default)
|
||||
/// 2. Config file values
|
||||
/// 3. Default values
|
||||
///
|
||||
/// CLI defaults are automatically inferred from clap's parsed defaults.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - The parsed command-line arguments
|
||||
#[must_use]
|
||||
pub fn to_ui_options(&self, args: &CommandLineArgs) -> UIOptions {
|
||||
// Get clap's defaults by parsing an empty argument list.
|
||||
// This lets us detect which CLI values were explicitly set vs. defaulted.
|
||||
let defaults = CommandLineArgs::default_values();
|
||||
|
||||
let enable_highlighting = merge_bool_setting(
|
||||
args.enable_highlighting,
|
||||
defaults.enable_highlighting,
|
||||
self.ui.syntax_highlighting,
|
||||
);
|
||||
let terminal_shell_integration = merge_bool_setting(
|
||||
args.terminal_shell_integration,
|
||||
defaults.terminal_shell_integration,
|
||||
self.experimental.terminal_shell_integration,
|
||||
);
|
||||
let zsh_style_hooks = merge_bool_setting(
|
||||
args.zsh_style_hooks,
|
||||
defaults.zsh_style_hooks,
|
||||
self.experimental.zsh_hooks,
|
||||
);
|
||||
|
||||
UIOptions::builder()
|
||||
.disable_bracketed_paste(args.disable_bracketed_paste)
|
||||
.disable_color(args.disable_color)
|
||||
.disable_highlighting(!enable_highlighting)
|
||||
.terminal_shell_integration(terminal_shell_integration)
|
||||
.zsh_style_hooks(zsh_style_hooks)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges a boolean setting from CLI args, config file, and defaults.
|
||||
///
|
||||
/// Priority: CLI (if explicitly set) > config file > default.
|
||||
///
|
||||
/// Since boolean CLI flags can't distinguish between "explicitly set to false" and
|
||||
/// "not provided" (both result in `false`), we use a heuristic:
|
||||
/// - If the CLI value differs from the default, the user explicitly provided it
|
||||
/// - Otherwise, use the config value if present, or fall back to the default
|
||||
const fn merge_bool_setting(
|
||||
cli_value: bool,
|
||||
cli_default: bool,
|
||||
config_value: Option<bool>,
|
||||
) -> bool {
|
||||
if cli_value != cli_default {
|
||||
// CLI was explicitly set to a non-default value
|
||||
cli_value
|
||||
} else if let Some(config) = config_value {
|
||||
// Use config file value
|
||||
config
|
||||
} else {
|
||||
// Fall back to default
|
||||
cli_default
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of attempting to load a configuration file.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ConfigLoadResult {
|
||||
/// The loaded configuration, or default if loading failed.
|
||||
pub config: Config,
|
||||
|
||||
/// The path that was used (or attempted) for loading.
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
/// Any error that occurred during loading.
|
||||
pub error: Option<ConfigLoadError>,
|
||||
|
||||
/// Whether the path was explicitly provided by the user (via `--config`).
|
||||
pub explicit_path: bool,
|
||||
}
|
||||
|
||||
impl ConfigLoadResult {
|
||||
/// Consumes the result and returns the configuration.
|
||||
///
|
||||
/// If an error occurred:
|
||||
/// - For explicit paths (user-provided via `--config`): returns `Err` with a formatted error
|
||||
/// - For default paths: logs a warning and returns the default configuration
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if an explicit config path was provided and loading failed.
|
||||
pub fn into_config_or_log(self) -> Result<Config, String> {
|
||||
let Some(err) = self.error else {
|
||||
return Ok(self.config);
|
||||
};
|
||||
|
||||
let path_display = self
|
||||
.path
|
||||
.as_ref()
|
||||
.map_or_else(|| String::from("<unknown>"), |p| p.display().to_string());
|
||||
|
||||
if self.explicit_path {
|
||||
// User explicitly provided --config; treat errors as fatal.
|
||||
return Err(format!("failed to load config from {path_display}: {err}"));
|
||||
}
|
||||
|
||||
// Default config path; log warning but continue with defaults.
|
||||
tracing::warn!("failed to load config from {path_display}: {err}");
|
||||
Ok(self.config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur when loading configuration.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigLoadError {
|
||||
/// Failed to read the configuration file.
|
||||
#[error("failed to read config file: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Failed to parse the TOML content.
|
||||
#[error("failed to parse config file: {0}")]
|
||||
Parse(#[from] toml::de::Error),
|
||||
}
|
||||
|
||||
const CONFIG_SUBDIR_NAME: &str = "brush";
|
||||
const CONFIG_FILE_NAME: &str = "config.toml";
|
||||
|
||||
/// Returns the default configuration file path for the current platform.
|
||||
///
|
||||
/// Uses the XDG Base Directory specification on Linux/macOS and appropriate
|
||||
/// platform conventions on other systems via the `etcetera` crate.
|
||||
///
|
||||
/// Returns `None` if the platform's config directory cannot be determined.
|
||||
pub fn default_config_path() -> Option<PathBuf> {
|
||||
let strategy = etcetera::choose_base_strategy().ok()?;
|
||||
Some(
|
||||
strategy
|
||||
.config_dir()
|
||||
.join(CONFIG_SUBDIR_NAME)
|
||||
.join(CONFIG_FILE_NAME),
|
||||
)
|
||||
}
|
||||
|
||||
/// Loads configuration from the specified path.
|
||||
///
|
||||
/// Returns a `ConfigLoadResult` containing:
|
||||
/// - The parsed configuration (or default on error)
|
||||
/// - The path that was used
|
||||
/// - Any error that occurred
|
||||
///
|
||||
/// Note: This function sets `explicit_path` to `false`. Use `load_config` for
|
||||
/// proper handling of explicit vs. default paths.
|
||||
pub fn load_from_path(path: &Path) -> ConfigLoadResult {
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
return ConfigLoadResult {
|
||||
path: Some(path.to_path_buf()),
|
||||
error: Some(ConfigLoadError::Io(e)),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
match toml::from_str(&content) {
|
||||
Ok(config) => ConfigLoadResult {
|
||||
config,
|
||||
path: Some(path.to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
Err(e) => ConfigLoadResult {
|
||||
path: Some(path.to_path_buf()),
|
||||
error: Some(ConfigLoadError::Parse(e)),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads configuration based on the provided options.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `disabled` - If true, skip loading and return defaults
|
||||
/// * `explicit_path` - If provided, use this path instead of the default
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `ConfigLoadResult` containing the configuration and any errors encountered.
|
||||
/// If `explicit_path` is provided and loading fails, the result will have
|
||||
/// `explicit_path: true` to indicate that the error should be treated as fatal.
|
||||
pub fn load_config(disabled: bool, explicit_path: Option<&Path>) -> ConfigLoadResult {
|
||||
if disabled {
|
||||
return ConfigLoadResult::default();
|
||||
}
|
||||
|
||||
let is_explicit = explicit_path.is_some();
|
||||
|
||||
let path = match explicit_path {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => match default_config_path() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
// Can't determine config path; use defaults silently
|
||||
return ConfigLoadResult::default();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// If using default path and file doesn't exist, silently use defaults
|
||||
if !is_explicit && !path.exists() {
|
||||
return ConfigLoadResult {
|
||||
path: Some(path),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
let mut result = load_from_path(&path);
|
||||
result.explicit_path = is_explicit;
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn empty_config() {
|
||||
let config: Config = toml::from_str("").unwrap();
|
||||
assert!(config.ui.syntax_highlighting.is_none());
|
||||
assert!(config.experimental.zsh_hooks.is_none());
|
||||
assert!(config.experimental.terminal_shell_integration.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_config() {
|
||||
let toml = r"
|
||||
[ui]
|
||||
syntax-highlighting = true
|
||||
|
||||
[experimental]
|
||||
zsh-hooks = true
|
||||
terminal-shell-integration = false
|
||||
";
|
||||
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.ui.syntax_highlighting, Some(true));
|
||||
assert_eq!(config.experimental.zsh_hooks, Some(true));
|
||||
assert_eq!(config.experimental.terminal_shell_integration, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_config() {
|
||||
let toml = r"
|
||||
[ui]
|
||||
syntax-highlighting = false
|
||||
";
|
||||
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.ui.syntax_highlighting, Some(false));
|
||||
assert!(config.experimental.zsh_hooks.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_fields_ignored() {
|
||||
let toml = r#"
|
||||
[ui]
|
||||
syntax-highlighting = true
|
||||
unknown-field = "should be ignored"
|
||||
another-unknown = 42
|
||||
|
||||
[experimental]
|
||||
zsh-hooks = false
|
||||
future-feature = true
|
||||
|
||||
[unknown-section]
|
||||
foo = "bar"
|
||||
"#;
|
||||
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.ui.syntax_highlighting, Some(true));
|
||||
assert_eq!(config.experimental.zsh_hooks, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_disabled() {
|
||||
let result = load_config(true, None);
|
||||
assert!(result.path.is_none());
|
||||
assert!(result.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_nonexistent_default() {
|
||||
// When using default path and file doesn't exist, should return defaults without error
|
||||
let result = load_config(false, None);
|
||||
// We may or may not get a path depending on platform, but shouldn't error
|
||||
assert!(result.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_nonexistent_explicit() {
|
||||
let path = Path::new("/nonexistent/path/to/config.toml");
|
||||
let result = load_config(false, Some(path));
|
||||
assert!(result.error.is_some());
|
||||
assert!(matches!(result.error, Some(ConfigLoadError::Io(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_ui_options_defaults_only() {
|
||||
let config = Config::default();
|
||||
let args = CommandLineArgs::default_values();
|
||||
let ui = config.to_ui_options(&args);
|
||||
|
||||
assert!(!ui.disable_bracketed_paste);
|
||||
assert!(!ui.disable_color);
|
||||
// Note: whether highlighting is enabled by default depends on the compile-time
|
||||
// DEFAULT_ENABLE_HIGHLIGHTING constant (true with reedline, false without)
|
||||
assert!(!ui.terminal_shell_integration);
|
||||
assert!(!ui.zsh_style_hooks);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_ui_options_config_overrides_defaults() {
|
||||
let toml = r"
|
||||
[ui]
|
||||
syntax-highlighting = true
|
||||
|
||||
[experimental]
|
||||
zsh-hooks = true
|
||||
terminal-shell-integration = true
|
||||
";
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
let args = CommandLineArgs::default_values();
|
||||
|
||||
// CLI values match defaults, so config should take effect
|
||||
let ui = config.to_ui_options(&args);
|
||||
|
||||
assert!(!ui.disable_highlighting); // config enabled highlighting
|
||||
assert!(ui.terminal_shell_integration);
|
||||
assert!(ui.zsh_style_hooks);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_ui_options_cli_overrides_config() {
|
||||
let toml = r"
|
||||
[ui]
|
||||
syntax-highlighting = false
|
||||
|
||||
[experimental]
|
||||
zsh-hooks = false
|
||||
";
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
|
||||
// Simulate CLI explicitly setting values different from defaults
|
||||
// by parsing with the flags enabled
|
||||
let args = CommandLineArgs::try_parse_from([
|
||||
"brush",
|
||||
"--enable-highlighting",
|
||||
"--enable-zsh-hooks",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
// CLI explicitly enables highlighting and zsh-hooks (differs from default)
|
||||
let ui = config.to_ui_options(&args);
|
||||
|
||||
assert!(!ui.disable_highlighting); // CLI enabled highlighting
|
||||
assert!(ui.zsh_style_hooks); // CLI enabled
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_ui_options_cli_only_settings() {
|
||||
let config = Config::default();
|
||||
let args = CommandLineArgs::try_parse_from([
|
||||
"brush",
|
||||
"--disable-bracketed-paste",
|
||||
"--disable-color",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let ui = config.to_ui_options(&args);
|
||||
|
||||
assert!(ui.disable_bracketed_paste);
|
||||
assert!(ui.disable_color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,819 @@
|
||||
//! Implements the command-line interface for the `brush` shell.
|
||||
|
||||
use crate::args::CommandLineArgs;
|
||||
use crate::args::InputBackendType;
|
||||
use crate::brushctl::ShellBuilderBrushBuiltinExt as _;
|
||||
use crate::bundled;
|
||||
use crate::config;
|
||||
use crate::error_formatter;
|
||||
use crate::events;
|
||||
use crate::productinfo;
|
||||
use brush_builtins::ShellBuilderExt as _;
|
||||
#[cfg(feature = "experimental-builtins")]
|
||||
use brush_experimental_builtins::ShellBuilderExt as _;
|
||||
use clap::CommandFactory;
|
||||
use std::sync::LazyLock;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[allow(unused_imports, reason = "only used in some configs")]
|
||||
use std::io::IsTerminal;
|
||||
|
||||
static TRACE_EVENT_CONFIG: LazyLock<Arc<tokio::sync::Mutex<Option<events::TraceEventConfig>>>> =
|
||||
LazyLock::new(|| Arc::new(tokio::sync::Mutex::new(None)));
|
||||
|
||||
type BrushShellExtensions = brush_core::extensions::ShellExtensionsImpl<error_formatter::Formatter>;
|
||||
type BrushShell = brush_core::Shell<BrushShellExtensions>;
|
||||
|
||||
// WARN: this implementation shadows `clap::Parser::parse_from` one so it must be defined
|
||||
// after the `use clap::Parser`
|
||||
impl CommandLineArgs {
|
||||
// Work around clap's limitation handling `--` like a regular value
|
||||
// TODO(cmdline): We can safely remove this `impl` after the issue is resolved
|
||||
// https://github.com/clap-rs/clap/issues/5055
|
||||
// This function takes precedence over [`clap::Parser::parse_from`]
|
||||
fn try_parse_from(itr: impl IntoIterator<Item = String>) -> Result<Self, clap::Error> {
|
||||
let mut args: Vec<String> = itr.into_iter().collect();
|
||||
|
||||
// In bash, `-c` treats `--` as an option terminator and takes its
|
||||
// command string from the first argument *after* `--`. (Other
|
||||
// value-taking flags like `-o` and `-O` instead consume `--` as their
|
||||
// literal value in bash, rejecting it as an invalid option name.)
|
||||
//
|
||||
// Remove the `--` so that `-c` naturally consumes the next token as its
|
||||
// value via clap. Other value-taking flags are unaffected: for them
|
||||
// try_parse_known splits at `--` before clap sees it, so they still
|
||||
// produce an error for invocations like `-o --`/`-O --` (via a missing
|
||||
// value rather than an invalid option name). In both cases, we
|
||||
// intentionally do not treat `--` as an option terminator for those
|
||||
// flags.
|
||||
if let Some(dd_idx) = args.iter().position(|a| a == "--") {
|
||||
if let Some(flag_idx) = dd_idx
|
||||
.checked_sub(1)
|
||||
.filter(|&i| Self::has_pending_c_flag(&args[i]))
|
||||
{
|
||||
// Remove the option-terminating `--`.
|
||||
args.remove(dd_idx);
|
||||
|
||||
// If the command value (now at dd_idx) is itself `--`, merge it
|
||||
// into the flag as an attached value (e.g., "-c" + "--" → "-c--").
|
||||
// Clap parses `-c--` as `-c` with value `"--"` (standard POSIX
|
||||
// short-option-with-attached-value syntax). This prevents
|
||||
// try_parse_known from splitting at it again.
|
||||
if args.get(dd_idx).map(String::as_str) == Some("--") {
|
||||
let value = args.remove(dd_idx);
|
||||
args[flag_idx].push_str(&value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (mut this, script_args) = brush_core::builtins::try_parse_known::<Self>(args)?;
|
||||
|
||||
// Collect any args from after `--` (handled by try_parse_known) into
|
||||
// script_args, which become positional parameters ($0, $1, ...).
|
||||
if let Some(args) = script_args {
|
||||
this.script_args.extend(args);
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
/// Returns true if `arg` is `-c` or a combined short-flag group ending in
|
||||
/// `c` (like `-ec`) where all preceding characters are boolean flags.
|
||||
///
|
||||
/// This specifically targets `-c` because it is the only short flag with
|
||||
/// special `--` option-terminator behavior in bash. Other value-taking flags
|
||||
/// (`-o`, `-O`) consume `--` as their literal value instead.
|
||||
///
|
||||
/// Uses clap's argument definitions to validate preceding flags, avoiding
|
||||
/// a hardcoded list of boolean flag characters.
|
||||
fn has_pending_c_flag(arg: &str) -> bool {
|
||||
// Must be a short flag group ending in 'c': "-c", "-ec", "-xec", etc.
|
||||
let Some(flags) = arg.strip_prefix('-') else {
|
||||
return false;
|
||||
};
|
||||
let Some(preceding) = flags.strip_suffix('c') else {
|
||||
return false;
|
||||
};
|
||||
// Reject long-option-like args (e.g., "--c").
|
||||
if preceding.starts_with('-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For "-c" alone, preceding is empty and the check below is vacuously
|
||||
// true. For combined flags like `-ec`, verify all chars before the
|
||||
// trailing `c` are boolean flags. If any preceding char takes a value
|
||||
// (like `o`), then `c` is consumed as that flag's value, not as `-c`.
|
||||
let cmd = Self::command();
|
||||
preceding.chars().all(|ch| {
|
||||
cmd.get_arguments().any(|a| {
|
||||
a.get_short() == Some(ch)
|
||||
&& !matches!(
|
||||
a.get_action(),
|
||||
clap::ArgAction::Set | clap::ArgAction::Append
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entry point for the `brush` shell.
|
||||
pub fn run() {
|
||||
//
|
||||
// Install the bundled-command registry so it's available both for
|
||||
// bundled dispatch (handled next) and for builtin shim registration
|
||||
// during shell construction. With no bundled-providing features enabled
|
||||
// the registry is empty and both code paths become no-ops.
|
||||
//
|
||||
bundled::install_default_providers();
|
||||
|
||||
//
|
||||
// If we were invoked as `brush <DISPATCH_FLAG> <name> [args...]`, run the
|
||||
// bundled command and exit before doing any shell setup. This is the
|
||||
// hot path for in-binary coreutils invocations.
|
||||
//
|
||||
if let Some(code) = bundled::maybe_dispatch() {
|
||||
std::process::exit(code);
|
||||
}
|
||||
|
||||
//
|
||||
// Install panic handlers to clean up on panic.
|
||||
//
|
||||
install_panic_handlers();
|
||||
|
||||
//
|
||||
// Parse args.
|
||||
//
|
||||
let mut args: Vec<_> = std::env::args().collect();
|
||||
|
||||
// Work around clap's limitations handling +O options.
|
||||
for arg in &mut args {
|
||||
if arg.starts_with("+O") {
|
||||
arg.insert_str(0, "--");
|
||||
}
|
||||
}
|
||||
|
||||
let parsed_args = match CommandLineArgs::try_parse_from(args.iter().cloned()) {
|
||||
Ok(parsed_args) => parsed_args,
|
||||
Err(e) => {
|
||||
let _ = e.print();
|
||||
|
||||
// Check for whether this is something we'd truly consider fatal. clap returns
|
||||
// errors for `--help`, `--version`, etc.
|
||||
let exit_code = match e.kind() {
|
||||
clap::error::ErrorKind::DisplayVersion => 0,
|
||||
clap::error::ErrorKind::DisplayHelp => 0,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Run.
|
||||
//
|
||||
#[cfg(any(unix, windows))]
|
||||
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
let mut builder = tokio::runtime::Builder::new_current_thread();
|
||||
|
||||
let Ok(runtime) = builder.enable_all().build() else {
|
||||
tracing::error!("error: failed to create Tokio runtime");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let result = runtime.block_on(run_async(args, parsed_args));
|
||||
|
||||
let exit_code = match result {
|
||||
Ok(code) => code,
|
||||
Err(err) => {
|
||||
tracing::error!("error: {err:#}");
|
||||
1
|
||||
}
|
||||
};
|
||||
|
||||
std::process::exit(i32::from(exit_code));
|
||||
}
|
||||
|
||||
/// Installs panic handlers to report our panic and cleanly exit on panic.
|
||||
fn install_panic_handlers() {
|
||||
//
|
||||
// Set up panic handler. On release builds, it will capture panic details to a
|
||||
// temporary .toml file and report a human-readable message to the screen.
|
||||
//
|
||||
human_panic::setup_panic!(
|
||||
human_panic::Metadata::new(productinfo::PRODUCT_NAME, productinfo::PRODUCT_VERSION)
|
||||
.homepage(env!("CARGO_PKG_HOMEPAGE"))
|
||||
.support("please post a GitHub issue at https://github.com/reubeno/brush/issues/new")
|
||||
);
|
||||
|
||||
//
|
||||
// If stdout is connected to a terminal, then register a new panic handler that
|
||||
// resets the terminal and then invokes the previously registered handler. In
|
||||
// dev/debug builds, the previously registered handler will be the default
|
||||
// handler; in release builds, it will be the one registered by `human_panic`.
|
||||
//
|
||||
if std::io::stdout().is_terminal() {
|
||||
let original_panic_handler = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// Best-effort attempt to reset the terminal to defaults.
|
||||
let _ = try_reset_terminal_to_defaults();
|
||||
|
||||
// Invoke the original handler
|
||||
original_panic_handler(panic_info);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental")]
|
||||
pub(crate) const DEFAULT_ENABLE_HIGHLIGHTING: bool = true;
|
||||
#[cfg(not(feature = "experimental"))]
|
||||
pub(crate) const DEFAULT_ENABLE_HIGHLIGHTING: bool = false;
|
||||
|
||||
/// Run the brush shell. Returns the exit code.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cli_args` - The command-line arguments to the shell, in string form.
|
||||
/// * `args` - The already-parsed command-line arguments.
|
||||
#[doc(hidden)]
|
||||
async fn run_async(
|
||||
cli_args: Vec<String>,
|
||||
args: CommandLineArgs,
|
||||
) -> Result<u8, brush_interactive::ShellError> {
|
||||
// Initializing tracing.
|
||||
let mut event_config = TRACE_EVENT_CONFIG.lock().await;
|
||||
*event_config = Some(events::TraceEventConfig::init(
|
||||
&args.enabled_debug_events,
|
||||
&args.disabled_events,
|
||||
));
|
||||
drop(event_config);
|
||||
|
||||
// Load configuration file.
|
||||
let file_config = config::load_config(args.no_config, args.config_file.as_deref())
|
||||
.into_config_or_log()
|
||||
.map_err(|e| brush_interactive::ShellError::IoError(std::io::Error::other(e)))?;
|
||||
|
||||
// Instantiate an appropriately configured shell and wrap it in an `Arc`. Note that we do
|
||||
// *not* run any code in the shell yet. We'll delay loading profiles and such until after
|
||||
// we've set up everything else (in `run_in_shell`).
|
||||
let shell: BrushShell = instantiate_shell(&args, cli_args).await?;
|
||||
let shell = Arc::new(Mutex::new(shell));
|
||||
|
||||
// Run with the selected input backend. Each branch instantiates the concrete
|
||||
// backend type and calls `run_in_shell`, preserving static dispatch.
|
||||
let default_backend = get_default_input_backend_type(&args);
|
||||
let selected_backend = args.input_backend.unwrap_or(default_backend);
|
||||
|
||||
// Build UI options by merging config file with CLI args.
|
||||
#[allow(unused_variables, reason = "not used when no backend features enabled")]
|
||||
let ui_options = file_config.to_ui_options(&args);
|
||||
|
||||
let result = match selected_backend {
|
||||
#[cfg(all(feature = "reedline", any(unix, windows)))]
|
||||
InputBackendType::Reedline => {
|
||||
let mut input_backend =
|
||||
brush_interactive::ReedlineInputBackend::new(&ui_options, &shell)?;
|
||||
run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await
|
||||
}
|
||||
#[cfg(any(not(feature = "reedline"), not(any(unix, windows))))]
|
||||
InputBackendType::Reedline => Err(brush_interactive::ShellError::InputBackendNotSupported),
|
||||
|
||||
#[cfg(feature = "basic")]
|
||||
InputBackendType::Basic => {
|
||||
let mut input_backend = brush_interactive::BasicInputBackend;
|
||||
run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await
|
||||
}
|
||||
#[cfg(not(feature = "basic"))]
|
||||
InputBackendType::Basic => Err(brush_interactive::ShellError::InputBackendNotSupported),
|
||||
|
||||
#[cfg(feature = "minimal")]
|
||||
InputBackendType::Minimal => {
|
||||
let mut input_backend = brush_interactive::MinimalInputBackend;
|
||||
run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await
|
||||
}
|
||||
#[cfg(not(feature = "minimal"))]
|
||||
InputBackendType::Minimal => Err(brush_interactive::ShellError::InputBackendNotSupported),
|
||||
};
|
||||
|
||||
// Display any error that percolated up.
|
||||
let exit_code = match result {
|
||||
Ok(code) => code,
|
||||
Err(brush_interactive::ShellError::ShellError(e)) => {
|
||||
let shell = shell.lock().await;
|
||||
let mut stderr = shell.stderr();
|
||||
let _ = shell.display_error(&mut stderr, &e);
|
||||
drop(shell);
|
||||
1
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("error: {err:#}");
|
||||
1
|
||||
}
|
||||
};
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
|
||||
/// Determines whether `run_in_shell` will run the shell interactively. Must be sync'd with it.
|
||||
const fn will_run_interactively(args: &CommandLineArgs) -> bool {
|
||||
if args.command.is_some() {
|
||||
false
|
||||
} else if args.read_commands_from_stdin {
|
||||
true
|
||||
} else {
|
||||
args.script_args.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the shell according to the provided command-line arguments.
|
||||
/// Also responsible for loading profiles and rc files as appropriate.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `shell_ref` - A reference to the shell to run.
|
||||
/// * `args` - The parsed command-line arguments.
|
||||
/// * `input_backend` - The input backend to use.
|
||||
/// * `ui_options` - The user interface options to use.
|
||||
async fn run_in_shell(
|
||||
shell_ref: &brush_interactive::ShellRef<impl brush_core::ShellExtensions>,
|
||||
args: CommandLineArgs,
|
||||
input_backend: &mut impl brush_interactive::InputBackend,
|
||||
ui_options: &brush_interactive::UIOptions,
|
||||
) -> Result<u8, brush_interactive::ShellError> {
|
||||
// First load profile and rc files as appropriate.
|
||||
initialize_shell(shell_ref, &args).await?;
|
||||
|
||||
// If a command was specified via -c, then run that command and then exit.
|
||||
if let Some(command) = args.command {
|
||||
shell_ref.lock().await.run_dash_c_command(command).await?;
|
||||
|
||||
// If -s was provided, then read commands from stdin. If there was a script (and optionally
|
||||
// args) passed on the command line via positional arguments, then we copy over the
|
||||
// parameters but do *not* execute it.
|
||||
} else if args.read_commands_from_stdin {
|
||||
let interactive_options = ui_options.into();
|
||||
brush_interactive::InteractiveShell::new(shell_ref, input_backend, &interactive_options)?
|
||||
.run_interactively()
|
||||
.await?;
|
||||
|
||||
// If a script path was provided, then run the script.
|
||||
} else if !args.script_args.is_empty() {
|
||||
// The path to a script was provided on the command line; run the script.
|
||||
shell_ref
|
||||
.lock()
|
||||
.await
|
||||
.run_script(
|
||||
Path::new(&args.script_args[0]),
|
||||
args.script_args.iter().skip(1),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// If we got down here, then we don't have any commands to run. We'll be reading
|
||||
// them in from stdin one way or the other.
|
||||
} else {
|
||||
let interactive_options = ui_options.into();
|
||||
brush_interactive::InteractiveShell::new(shell_ref, input_backend, &interactive_options)?
|
||||
.run_interactively()
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Make sure to return the last result observed in the shell.
|
||||
let result = shell_ref.lock().await.last_exit_status();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Initializes a shell by loading profile and rc files as appropriate.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `shell_ref` - A reference to the shell to initialize.
|
||||
/// * `args` - The parsed command-line arguments.
|
||||
async fn initialize_shell(
|
||||
shell_ref: &brush_interactive::ShellRef<impl brush_core::ShellExtensions>,
|
||||
args: &CommandLineArgs,
|
||||
) -> Result<(), brush_interactive::ShellError> {
|
||||
// Compute desired profile-loading behavior.
|
||||
let profile = if args.no_profile {
|
||||
brush_core::ProfileLoadBehavior::Skip
|
||||
} else {
|
||||
brush_core::ProfileLoadBehavior::LoadDefault
|
||||
};
|
||||
|
||||
// Compute desired rc-loading behavior.
|
||||
let rc = if args.no_rc {
|
||||
brush_core::RcLoadBehavior::Skip
|
||||
} else if let Some(rc_file) = &args.rc_file {
|
||||
brush_core::RcLoadBehavior::LoadCustom(rc_file.clone())
|
||||
} else {
|
||||
brush_core::RcLoadBehavior::LoadDefault
|
||||
};
|
||||
|
||||
shell_ref.lock().await.load_config(&profile, &rc).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Instantiates a shell from command-line arguments. Does *not* run any code in the shell.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - The parsed command-line arguments.
|
||||
/// * `cli_args` - The raw command-line arguments.
|
||||
async fn instantiate_shell(
|
||||
args: &CommandLineArgs,
|
||||
cli_args: Vec<String>,
|
||||
) -> Result<BrushShell, brush_interactive::ShellError> {
|
||||
#[cfg(feature = "experimental-load")]
|
||||
let mut shell = if let Some(load_file) = &args.load_file {
|
||||
instantiate_shell_from_file(load_file.as_path())?
|
||||
} else {
|
||||
instantiate_shell_from_args(args, cli_args).await?
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "experimental-load"))]
|
||||
let mut shell = instantiate_shell_from_args(args, cli_args).await?;
|
||||
|
||||
// Register shims for any bundled commands in the installed registry.
|
||||
// Done here (not inside the inner instantiators) so both paths are
|
||||
// covered from a single site.
|
||||
bundled::register_shims(&mut shell);
|
||||
|
||||
Ok(shell)
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-load")]
|
||||
fn instantiate_shell_from_file(
|
||||
file_path: &Path,
|
||||
) -> Result<BrushShell, brush_interactive::ShellError> {
|
||||
let mut shell: BrushShell = serde_json::from_reader(std::fs::File::open(file_path)?)
|
||||
.map_err(|e| brush_interactive::ShellError::IoError(std::io::Error::other(e)))?;
|
||||
|
||||
// NOTE: We need to manually register builtins because we can't serialize/deserialize them.
|
||||
// TODO(serde): we should consider whether we could/should at least track *which* are enabled.
|
||||
let builtin_set = if shell.options().sh_mode {
|
||||
brush_builtins::BuiltinSet::ShMode
|
||||
} else {
|
||||
brush_builtins::BuiltinSet::BashMode
|
||||
};
|
||||
|
||||
let builtins = brush_builtins::default_builtins(builtin_set);
|
||||
|
||||
for (builtin_name, builtin) in builtins {
|
||||
shell.register_builtin(&builtin_name, builtin);
|
||||
}
|
||||
|
||||
// Add experimental builtins (if enabled).
|
||||
#[cfg(feature = "experimental-builtins")]
|
||||
for (builtin_name, builtin) in brush_experimental_builtins::experimental_builtins() {
|
||||
shell.register_builtin(&builtin_name, builtin);
|
||||
}
|
||||
|
||||
Ok(shell)
|
||||
}
|
||||
|
||||
/// Instantiates a shell from command-line arguments. Does *not* run any code in the shell.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - The parsed command-line arguments.
|
||||
/// * `cli_args` - The raw command-line arguments.
|
||||
async fn instantiate_shell_from_args(
|
||||
args: &CommandLineArgs,
|
||||
cli_args: Vec<String>,
|
||||
) -> Result<BrushShell, brush_interactive::ShellError> {
|
||||
// Compute login flag.
|
||||
let login = args.login || cli_args.first().is_some_and(|argv0| argv0.starts_with('-'));
|
||||
|
||||
// Compute shell name.
|
||||
let shell_name = if args.command.is_some() && !args.script_args.is_empty() {
|
||||
Some(args.script_args[0].clone())
|
||||
} else if !cli_args.is_empty() {
|
||||
Some(cli_args[0].clone())
|
||||
} else if args.sh_mode {
|
||||
// Simulate having been run as "sh".
|
||||
Some(String::from("sh"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Compute positional shell arguments.
|
||||
let shell_args = if args.command.is_some() {
|
||||
Some(args.script_args.iter().skip(1).cloned().collect())
|
||||
} else if args.read_commands_from_stdin {
|
||||
Some(args.script_args.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Commands are read from stdin if -s was provided, or if no command was specified (either via
|
||||
// -c or as a positional argument).
|
||||
let read_commands_from_stdin = (args.read_commands_from_stdin && args.command.is_none())
|
||||
|| (args.script_args.is_empty() && args.command.is_none());
|
||||
|
||||
let builtin_set = if args.sh_mode {
|
||||
brush_builtins::BuiltinSet::ShMode
|
||||
} else {
|
||||
brush_builtins::BuiltinSet::BashMode
|
||||
};
|
||||
|
||||
// Identify the file descriptors to inherit.
|
||||
let fds = args
|
||||
.inherited_fds
|
||||
.iter()
|
||||
.filter_map(|&fd| brush_core::sys::fd::try_get_file_for_open_fd(fd).map(|file| (fd, file)))
|
||||
.collect();
|
||||
|
||||
// Select parser implementation to use.
|
||||
#[cfg(feature = "experimental-parser")]
|
||||
let parser_impl = if args.experimental_parser {
|
||||
brush_core::parser::ParserImpl::Winnow
|
||||
} else {
|
||||
brush_core::parser::ParserImpl::Peg
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "experimental-parser"))]
|
||||
let parser_impl = brush_core::parser::ParserImpl::Peg;
|
||||
|
||||
// Set up the shell builder with the requested options.
|
||||
// NOTE: We skip loading profile and rc files here; that will be handled later after we've
|
||||
// fully instantiated everything we want set before running any code.
|
||||
let shell = brush_core::Shell::builder_with_extensions::<BrushShellExtensions>()
|
||||
.disable_options(args.disabled_options.clone())
|
||||
.disable_shopt_options(args.disabled_shopt_options.clone())
|
||||
.disallow_overwriting_regular_files_via_output_redirection(
|
||||
args.disallow_overwriting_regular_files_via_output_redirection,
|
||||
)
|
||||
.enable_options(args.enabled_options.clone())
|
||||
.enable_shopt_options(args.enabled_shopt_options.clone())
|
||||
.do_not_execute_commands(args.do_not_execute_commands)
|
||||
.exit_after_one_command(args.exit_after_one_command)
|
||||
.login(login)
|
||||
.interactive(args.is_interactive())
|
||||
.command_string_mode(args.command.is_some())
|
||||
.no_editing(args.no_editing)
|
||||
.profile(brush_core::ProfileLoadBehavior::Skip)
|
||||
.rc(brush_core::RcLoadBehavior::Skip)
|
||||
.do_not_inherit_env(args.do_not_inherit_env)
|
||||
.fds(fds)
|
||||
.maybe_shell_args(shell_args)
|
||||
.posix(args.posix || args.sh_mode)
|
||||
.print_commands_and_arguments(args.print_commands_and_arguments)
|
||||
.read_commands_from_stdin(read_commands_from_stdin)
|
||||
.maybe_shell_name(shell_name)
|
||||
.shell_product_display_str(productinfo::get_product_display_str())
|
||||
.sh_mode(args.sh_mode)
|
||||
.treat_unset_variables_as_error(args.treat_unset_variables_as_error)
|
||||
.exit_on_nonzero_command_exit(args.exit_on_nonzero_command_exit)
|
||||
.disable_pathname_expansion(args.disable_pathname_expansion)
|
||||
.verbose(args.verbose)
|
||||
.parser(parser_impl)
|
||||
.error_formatter(new_error_behavior(args))
|
||||
.shell_version(env!("CARGO_PKG_VERSION").to_string());
|
||||
|
||||
// Add builtins.
|
||||
let shell = shell.default_builtins(builtin_set).brush_builtins();
|
||||
|
||||
// Add experimental builtins (if enabled).
|
||||
#[cfg(feature = "experimental-builtins")]
|
||||
let shell = shell.experimental_builtins();
|
||||
|
||||
// Build the shell.
|
||||
let mut shell = shell.build().await?;
|
||||
|
||||
// Make adjustments.
|
||||
if let Some(xtrace_file_path) = &args.xtrace_file_path {
|
||||
enable_xtrace_to_file(&mut shell, xtrace_file_path)?;
|
||||
}
|
||||
|
||||
Ok(shell)
|
||||
}
|
||||
|
||||
fn enable_xtrace_to_file(
|
||||
shell: &mut brush_core::Shell<impl brush_core::ShellExtensions>,
|
||||
file_path: &Path,
|
||||
) -> Result<(), brush_interactive::ShellError> {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(file_path)
|
||||
.map_err(|e| {
|
||||
brush_interactive::ShellError::FailedToCreateXtraceFile(file_path.to_path_buf(), e)
|
||||
})?;
|
||||
|
||||
let file = brush_core::openfiles::OpenFile::from(file);
|
||||
let file_fd = shell.open_files_mut().add(file)?;
|
||||
|
||||
shell.options_mut().print_commands_and_arguments = true;
|
||||
shell.set_env_global(
|
||||
"BASH_XTRACEFD",
|
||||
brush_core::ShellVariable::new(file_fd.to_string()),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn new_error_behavior(args: &CommandLineArgs) -> error_formatter::Formatter {
|
||||
error_formatter::Formatter {
|
||||
use_color: !args.disable_color,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_default_input_backend_type(args: &CommandLineArgs) -> InputBackendType {
|
||||
#[cfg(any(unix, windows))]
|
||||
{
|
||||
// If stdin isn't a terminal, then `reedline` doesn't do the right thing
|
||||
// (reference: https://github.com/nushell/reedline/issues/509). Switch to
|
||||
// the minimal input backend instead for that scenario.
|
||||
if std::io::stdin().is_terminal() && will_run_interactively(args) {
|
||||
InputBackendType::Reedline
|
||||
} else {
|
||||
InputBackendType::Minimal
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
let _args = args;
|
||||
InputBackendType::Minimal
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_event_config() -> Arc<tokio::sync::Mutex<Option<events::TraceEventConfig>>> {
|
||||
TRACE_EVENT_CONFIG.clone()
|
||||
}
|
||||
|
||||
fn try_reset_terminal_to_defaults() -> Result<(), std::io::Error> {
|
||||
#[cfg(any(unix, windows))]
|
||||
{
|
||||
// Reset the console.
|
||||
let exec_result = crossterm::execute!(
|
||||
std::io::stdout(),
|
||||
crossterm::terminal::LeaveAlternateScreen,
|
||||
crossterm::terminal::EnableLineWrap,
|
||||
crossterm::style::ResetColor,
|
||||
crossterm::event::DisableMouseCapture,
|
||||
crossterm::event::DisableBracketedPaste,
|
||||
crossterm::cursor::Show,
|
||||
crossterm::cursor::MoveToNextLine(1),
|
||||
);
|
||||
|
||||
let raw_result = crossterm::terminal::disable_raw_mode();
|
||||
|
||||
exec_result?;
|
||||
raw_result?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::panic_in_result_fn)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::{assert_eq, assert_matches};
|
||||
|
||||
fn args(strs: &[&str]) -> Vec<String> {
|
||||
strs.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_args() -> Result<()> {
|
||||
let parsed_args = CommandLineArgs::try_parse_from(args(&["brush"]))?;
|
||||
assert_matches!(parsed_args.script_args.as_slice(), []);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_script_and_args() -> Result<()> {
|
||||
let parsed_args = CommandLineArgs::try_parse_from(args(&[
|
||||
"brush",
|
||||
"some-script",
|
||||
"-x",
|
||||
"1",
|
||||
"--option",
|
||||
]))?;
|
||||
assert_eq!(
|
||||
parsed_args.script_args,
|
||||
["some-script", "-x", "1", "--option"]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_script_and_args_with_double_dash_in_script_args() -> Result<()> {
|
||||
let parsed_args = CommandLineArgs::try_parse_from(args(&["brush", "some-script", "--"]))?;
|
||||
assert_eq!(parsed_args.script_args, ["some-script", "--"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_unknown_args() {
|
||||
let result = CommandLineArgs::try_parse_from(args(&["brush", "--unknown-option"]));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_c_with_double_dash_separator() -> Result<()> {
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-c", "--", "echo hello", "arg0"]))?;
|
||||
assert_eq!(parsed_args.command, Some("echo hello".to_string()));
|
||||
assert_eq!(parsed_args.script_args, ["arg0"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_c_with_double_dash_no_command() {
|
||||
assert!(CommandLineArgs::try_parse_from(args(&["brush", "-c", "--"])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_c_with_double_dash_command_is_double_dash() -> Result<()> {
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-c", "--", "--", "echo", "hi"]))?;
|
||||
assert_eq!(parsed_args.command, Some("--".to_string()));
|
||||
assert_eq!(parsed_args.script_args, ["echo", "hi"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ec_with_double_dash_separator() -> Result<()> {
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-ec", "--", "echo hello", "arg0"]))?;
|
||||
assert_eq!(parsed_args.command, Some("echo hello".to_string()));
|
||||
assert!(parsed_args.exit_on_nonzero_command_exit);
|
||||
assert_eq!(parsed_args.script_args, ["arg0"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_c_with_value_before_double_dash_unchanged() -> Result<()> {
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-c", "echo hi", "--", "arg0"]))?;
|
||||
assert_eq!(parsed_args.command, Some("echo hi".to_string()));
|
||||
assert_eq!(parsed_args.script_args, ["--", "arg0"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_o_with_double_dash_is_not_transformed() {
|
||||
// Unlike -c, bash's -o consumes -- as its literal value (invalid option
|
||||
// name), not as an option terminator. Verify we don't transform it.
|
||||
let result = CommandLineArgs::try_parse_from(args(&["brush", "-o", "--"]));
|
||||
// Here, try_parse_from / try_parse_known splits at --, so -o ends up
|
||||
// without a value and parsing correctly fails. The key assertion is
|
||||
// that we MUST NOT reinterpret -- as an option terminator for -o and
|
||||
// then take any later argument as its value.
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_oc_not_treated_as_pending_c() -> Result<()> {
|
||||
// -oc means -o with value "c", not -o flag + -c flag. The --
|
||||
// should NOT be treated as an option terminator for -c.
|
||||
let parsed_args = CommandLineArgs::try_parse_from(args(&["brush", "-oc", "--", "echo"]))?;
|
||||
// -o consumed "c" as its value; -- split the rest; no -c command.
|
||||
assert!(parsed_args.command.is_none());
|
||||
assert_eq!(parsed_args.script_args, ["--", "echo"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bool_flag_before_double_dash_not_transformed() -> Result<()> {
|
||||
// -e is a boolean flag, not -c. The -- should NOT be removed;
|
||||
// everything from -- onward becomes positional (including -c).
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-e", "--", "-c", "echo"]))?;
|
||||
assert!(parsed_args.command.is_none());
|
||||
assert!(parsed_args.exit_on_nonzero_command_exit);
|
||||
assert_eq!(parsed_args.script_args, ["--", "-c", "echo"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_c_with_double_dash_and_later_double_dash() -> Result<()> {
|
||||
// After removing the first --, -c gets "echo". The second -- is
|
||||
// handled by try_parse_known and appears in script_args.
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-c", "--", "echo", "--", "more"]))?;
|
||||
assert_eq!(parsed_args.command, Some("echo".to_string()));
|
||||
assert_eq!(parsed_args.script_args, ["--", "more"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_pending_c_flag_edge_cases() {
|
||||
// Direct tests for the detection function.
|
||||
assert!(CommandLineArgs::has_pending_c_flag("-c"));
|
||||
assert!(CommandLineArgs::has_pending_c_flag("-ec"));
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("-C")); // uppercase, different flag
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("-oc")); // -o takes a value
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("--c")); // long-option-like
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("-")); // bare dash
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("c")); // no leading dash
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("")); // empty
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct Formatter {
|
||||
pub use_color: bool,
|
||||
}
|
||||
|
||||
impl brush_core::extensions::ErrorFormatter for Formatter {
|
||||
fn format_error(
|
||||
&self,
|
||||
err: &brush_core::error::Error,
|
||||
_shell: &brush_core::Shell<impl brush_core::ShellExtensions>,
|
||||
) -> String {
|
||||
let prefix = if self.use_color {
|
||||
color_print::cstr!("<red>error:</red> ")
|
||||
} else {
|
||||
"error: "
|
||||
};
|
||||
|
||||
std::format!("{prefix}{err:#}\n")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//! Facilities for configuring event tracing in the brush shell.
|
||||
|
||||
use std::{collections::HashSet, fmt::Display};
|
||||
|
||||
use brush_core::Error;
|
||||
use tracing_subscriber::{
|
||||
Layer, Registry, filter::Targets, layer::SubscriberExt, reload::Handle, util::SubscriberInitExt,
|
||||
};
|
||||
|
||||
/// Type of event to trace.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
|
||||
pub enum TraceEvent {
|
||||
/// Traces parsing and evaluation of arithmetic expressions.
|
||||
#[clap(name = "arithmetic")]
|
||||
Arithmetic,
|
||||
/// Traces command execution.
|
||||
#[clap(name = "commands")]
|
||||
Commands,
|
||||
/// Traces command completion generation.
|
||||
#[clap(name = "complete")]
|
||||
Complete,
|
||||
/// Traces word expansion.
|
||||
#[clap(name = "expand")]
|
||||
Expand,
|
||||
/// Traces functions.
|
||||
#[clap(name = "functions")]
|
||||
Functions,
|
||||
/// Traces input controls.
|
||||
#[clap(name = "input")]
|
||||
Input,
|
||||
/// Traces job management.
|
||||
#[clap(name = "jobs")]
|
||||
Jobs,
|
||||
/// Traces the process of parsing tokens into an abstract syntax tree.
|
||||
#[clap(name = "parse")]
|
||||
Parse,
|
||||
/// Traces pattern matching.
|
||||
#[clap(name = "pattern")]
|
||||
Pattern,
|
||||
/// Traces the process of tokenizing input text.
|
||||
#[clap(name = "tokenize")]
|
||||
Tokenize,
|
||||
/// Traces usage of unimplemented functionality.
|
||||
#[clap(name = "unimplemented", alias = "unimp")]
|
||||
Unimplemented,
|
||||
}
|
||||
|
||||
impl Display for TraceEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Arithmetic => write!(f, "arithmetic"),
|
||||
Self::Commands => write!(f, "commands"),
|
||||
Self::Complete => write!(f, "complete"),
|
||||
Self::Expand => write!(f, "expand"),
|
||||
Self::Functions => write!(f, "functions"),
|
||||
Self::Input => write!(f, "input"),
|
||||
Self::Jobs => write!(f, "jobs"),
|
||||
Self::Parse => write!(f, "parse"),
|
||||
Self::Pattern => write!(f, "pattern"),
|
||||
Self::Tokenize => write!(f, "tokenize"),
|
||||
Self::Unimplemented => write!(f, "unimplemented"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TraceEventConfig {
|
||||
enabled_debug_events: HashSet<TraceEvent>,
|
||||
disabled_events: HashSet<TraceEvent>,
|
||||
handle: Option<Handle<Targets, Registry>>,
|
||||
}
|
||||
|
||||
impl TraceEventConfig {
|
||||
pub fn init(enabled_debug_events: &[TraceEvent], disabled_events: &[TraceEvent]) -> Self {
|
||||
let enabled_debug_events: HashSet<TraceEvent> =
|
||||
enabled_debug_events.iter().copied().collect();
|
||||
let disabled_events: HashSet<TraceEvent> = disabled_events.iter().copied().collect();
|
||||
|
||||
let mut config = Self {
|
||||
enabled_debug_events,
|
||||
disabled_events,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let filter = config.compose_filter();
|
||||
|
||||
// Make the filter reloadable so that we can change the log level at runtime.
|
||||
let (reload_filter, handle) = tracing_subscriber::reload::Layer::new(filter);
|
||||
|
||||
let layer = tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::io::stderr)
|
||||
.without_time()
|
||||
.with_target(false)
|
||||
.with_filter(reload_filter);
|
||||
|
||||
if tracing_subscriber::registry()
|
||||
.with(layer)
|
||||
.try_init()
|
||||
.is_ok()
|
||||
{
|
||||
config.handle = Some(handle);
|
||||
} else {
|
||||
// Something went wrong; proceed on anyway but complain audibly.
|
||||
eprintln!("warning: failed to initialize tracing.");
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn compose_filter(&self) -> tracing_subscriber::filter::Targets {
|
||||
let mut filter = tracing_subscriber::filter::Targets::new()
|
||||
.with_default(tracing_subscriber::filter::LevelFilter::INFO);
|
||||
|
||||
for event in &self.enabled_debug_events {
|
||||
let targets = Self::event_to_tracing_targets(event);
|
||||
filter = filter.with_targets(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| (target, tracing::Level::DEBUG)),
|
||||
);
|
||||
}
|
||||
|
||||
for event in &self.disabled_events {
|
||||
let targets = Self::event_to_tracing_targets(event);
|
||||
filter = filter.with_targets(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| (target, tracing::level_filters::LevelFilter::OFF)),
|
||||
);
|
||||
}
|
||||
|
||||
filter
|
||||
}
|
||||
|
||||
fn event_to_tracing_targets(event: &TraceEvent) -> Vec<&str> {
|
||||
match event {
|
||||
TraceEvent::Arithmetic => vec!["arithmetic"],
|
||||
TraceEvent::Commands => vec!["commands"],
|
||||
TraceEvent::Complete => vec!["completion"],
|
||||
TraceEvent::Expand => vec!["expansion"],
|
||||
TraceEvent::Functions => vec!["functions"],
|
||||
TraceEvent::Input => vec!["input"],
|
||||
TraceEvent::Jobs => vec!["jobs"],
|
||||
TraceEvent::Parse => vec!["parse"],
|
||||
TraceEvent::Pattern => vec!["pattern"],
|
||||
TraceEvent::Tokenize => vec!["tokenize"],
|
||||
TraceEvent::Unimplemented => vec!["unimplemented"],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn get_enabled_events(&self) -> &HashSet<TraceEvent> {
|
||||
&self.enabled_debug_events
|
||||
}
|
||||
|
||||
pub fn enable(&mut self, event: TraceEvent) -> Result<(), Error> {
|
||||
// Don't bother to reload config if nothing has changed.
|
||||
if !self.enabled_debug_events.insert(event) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.reload_filter()
|
||||
}
|
||||
|
||||
pub fn disable(&mut self, event: TraceEvent) -> Result<(), Error> {
|
||||
// Don't bother to reload config if nothing has changed.
|
||||
if !self.enabled_debug_events.remove(&event) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.reload_filter()
|
||||
}
|
||||
|
||||
fn reload_filter(&self) -> Result<(), Error> {
|
||||
if let Some(handle) = &self.handle {
|
||||
if handle.reload(self.compose_filter()).is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(brush_core::ErrorKind::Unimplemented("failed to enable tracing events").into())
|
||||
}
|
||||
} else {
|
||||
Err(brush_core::ErrorKind::Unimplemented("tracing not initialized").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! Create for brush, an executable bash-compatible shell.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod args;
|
||||
mod brushctl;
|
||||
pub mod bundled;
|
||||
pub mod config;
|
||||
pub mod entry;
|
||||
mod error_formatter;
|
||||
pub mod events;
|
||||
mod productinfo;
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Main entry for the `brush` shell.
|
||||
|
||||
fn main() {
|
||||
brush_shell::entry::run();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Information about this shell project.
|
||||
|
||||
// nash: rebranded from "brush" for the Nucleic agent shell fork (docs/NASH.md §2).
|
||||
/// The formal name of this product.
|
||||
pub const PRODUCT_NAME: &str = "nash";
|
||||
|
||||
const PRODUCT_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
|
||||
const PRODUCT_REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
|
||||
/// The URI to display as the product's homepage.
|
||||
#[allow(clippy::const_is_empty)]
|
||||
pub const PRODUCT_DISPLAY_URI: &str = if !PRODUCT_HOMEPAGE.is_empty() {
|
||||
PRODUCT_HOMEPAGE
|
||||
} else {
|
||||
PRODUCT_REPO
|
||||
};
|
||||
|
||||
/// The version of the product, in string form.
|
||||
pub const PRODUCT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Info regarding the specific version of sources used to build this product.
|
||||
pub const PRODUCT_GIT_VERSION: &str = git_version::git_version!(
|
||||
prefix = "git:",
|
||||
cargo_prefix = "cargo:",
|
||||
fallback = "unknown:",
|
||||
args = ["--always", "--dirty=-modified", "--match", ""]
|
||||
);
|
||||
|
||||
pub(crate) fn get_product_display_str() -> String {
|
||||
// nash: identify as the Nucleic agent shell fork of brush.
|
||||
std::format!(
|
||||
"{PRODUCT_NAME} (Nucleic agent shell, brush fork) {PRODUCT_VERSION} ({PRODUCT_GIT_VERSION}) - {PRODUCT_DISPLAY_URI}"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
name: "CLI tests"
|
||||
cases:
|
||||
- name: "Basic echo"
|
||||
stdin: "echo hello world"
|
||||
expected_exit_code: 0
|
||||
expected_stdout: "hello world\n"
|
||||
|
||||
- name: "Exit code propagates"
|
||||
stdin: "exit 42"
|
||||
expected_exit_code: 42
|
||||
# WASI 0.2's wasi-cli/exit interface only models success/failure, so any
|
||||
# non-zero exit code is clamped to 1 by the host (e.g., wasmtime).
|
||||
incompatible_platforms: ["wasi"]
|
||||
|
||||
- name: "Echo via args"
|
||||
args:
|
||||
- "-c"
|
||||
- "echo from args"
|
||||
expected_exit_code: 0
|
||||
expected_stdout: "from args\n"
|
||||
|
||||
- name: "Version flag succeeds"
|
||||
args:
|
||||
- "--version"
|
||||
expected_exit_code: 0
|
||||
# Note: can't easily check stdout content as it varies
|
||||
|
||||
- name: "Help flag succeeds"
|
||||
args:
|
||||
- "--help"
|
||||
expected_exit_code: 0
|
||||
|
||||
- name: "Invalid option fails"
|
||||
args:
|
||||
- "--unknown-argument-here"
|
||||
expected_exit_code: 2
|
||||
# Exit code 2 for argument parsing errors.
|
||||
# WASI 0.2's wasi-cli/exit interface only models success/failure, so any
|
||||
# non-zero exit code is clamped to 1 by the host (e.g., wasmtime).
|
||||
incompatible_platforms: ["wasi"]
|
||||
|
||||
- name: "Explicit config file not found fails"
|
||||
removed_default_args:
|
||||
- "--no-config"
|
||||
args:
|
||||
- "--config"
|
||||
- "/nonexistent/path/to/config.toml"
|
||||
- "-c"
|
||||
- "echo should_not_run"
|
||||
expected_exit_code: 1
|
||||
|
||||
- name: "Explicit config file valid"
|
||||
removed_default_args:
|
||||
- "--no-config"
|
||||
test_files:
|
||||
- path: "config.toml"
|
||||
contents: |
|
||||
[ui]
|
||||
syntax-highlighting = false
|
||||
|
||||
[experimental]
|
||||
zsh-hooks = false
|
||||
args:
|
||||
- "--config"
|
||||
- "config.toml"
|
||||
- "-c"
|
||||
- "echo config_loaded"
|
||||
expected_exit_code: 0
|
||||
expected_stdout: "config_loaded\n"
|
||||
|
||||
- name: "Config file with unknown fields succeeds"
|
||||
removed_default_args:
|
||||
- "--no-config"
|
||||
test_files:
|
||||
- path: "config.toml"
|
||||
contents: |
|
||||
[ui]
|
||||
syntax-highlighting = true
|
||||
future-setting = "ignored"
|
||||
|
||||
[unknown-section]
|
||||
foo = "bar"
|
||||
args:
|
||||
- "--config"
|
||||
- "config.toml"
|
||||
- "-c"
|
||||
- "echo forward_compat"
|
||||
expected_exit_code: 0
|
||||
expected_stdout: "forward_compat\n"
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: brush-test-harness/src/runner.rs
|
||||
expression: snapshot_content
|
||||
---
|
||||
exit_code: 0
|
||||
stdout: |
|
||||
hello
|
||||
stderr: ""
|
||||
files:
|
||||
trace.txt: |
|
||||
+ echo hello
|
||||
+ true
|
||||
@@ -0,0 +1,14 @@
|
||||
name: "Tracing tests"
|
||||
cases:
|
||||
- name: "xtrace-file captures trace output"
|
||||
args: ["--xtrace-file", "trace.txt"]
|
||||
stdin: |
|
||||
echo hello
|
||||
true
|
||||
expected_exit_code: 0
|
||||
snapshot: true
|
||||
# std::fs::File::try_clone() returns "operation not supported" on
|
||||
# wasm32-wasip2, so brush's OpenFile::clone() falls back to a failing
|
||||
# writer and the trace file ends up empty. Affects all output redirection
|
||||
# under wasi, not just xtrace.
|
||||
incompatible_platforms: ["wasi"]
|
||||
@@ -0,0 +1,83 @@
|
||||
name: "and/or"
|
||||
cases:
|
||||
- name: "Basic &&"
|
||||
stdin: |
|
||||
false && echo 1
|
||||
true && echo 2
|
||||
|
||||
- name: "Basic ||"
|
||||
stdin: |
|
||||
false || echo 1
|
||||
true || echo 2
|
||||
|
||||
- name: "Longer chains"
|
||||
stdin: |
|
||||
false || false || false || echo "Got to the end"
|
||||
echo "1" && echo "2" && echo "3" && echo "4"
|
||||
|
||||
- name: "Mixed chains"
|
||||
stdin: |
|
||||
false && true || echo "1. Got to the end"
|
||||
false && false || echo "2. Got to the end"
|
||||
true && false || echo "3. Got to the end"
|
||||
|
||||
- name: "And with return in function"
|
||||
stdin: |
|
||||
f() {
|
||||
echo "Before" && return 42 && echo "After return"
|
||||
echo "After and-or"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "And with exit"
|
||||
stdin: |
|
||||
echo "Before" && exit 42 && echo "After exit"
|
||||
echo "After and-or"
|
||||
|
||||
- name: "And with break in loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Iteration $i" && break && echo "After break"
|
||||
echo "After and-or"
|
||||
done
|
||||
echo "After loop"
|
||||
|
||||
- name: "And with continue in loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Iteration $i" && continue && echo "After continue"
|
||||
echo "After and-or"
|
||||
done
|
||||
echo "After loop"
|
||||
|
||||
- name: "Or with return in function"
|
||||
stdin: |
|
||||
f() {
|
||||
false || return 42 || echo "After return"
|
||||
echo "After or-list"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "Or with exit"
|
||||
stdin: |
|
||||
false || exit 42 || echo "After exit"
|
||||
echo "After or-list"
|
||||
|
||||
- name: "Or with break in loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
false || break || echo "After break"
|
||||
echo "After or-list"
|
||||
done
|
||||
echo "After loop"
|
||||
|
||||
- name: "Or with continue in loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Iteration $i"
|
||||
false || continue || echo "After continue"
|
||||
echo "After or-list"
|
||||
done
|
||||
echo "After loop"
|
||||
@@ -0,0 +1,122 @@
|
||||
name: "Argument handling tests"
|
||||
common_test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo \"$0\" \"$1\" \"$2\" \"$@\"
|
||||
|
||||
cases:
|
||||
- name: "-c mode arguments without --"
|
||||
args:
|
||||
- "-c"
|
||||
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
|
||||
- 1
|
||||
- "-2"
|
||||
- "3"
|
||||
|
||||
- name: "-c mode arguments with --"
|
||||
args:
|
||||
- "-c"
|
||||
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
|
||||
- "--"
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
- name: "-c mode and arguments with +O"
|
||||
args:
|
||||
- "+O"
|
||||
- "nullglob"
|
||||
- "-c"
|
||||
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
|
||||
- "--"
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
- name: "-c mode -- torture"
|
||||
args:
|
||||
- "-c"
|
||||
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
|
||||
- --
|
||||
- --
|
||||
- -&-1
|
||||
- --!
|
||||
- "\"-2\""
|
||||
- "''--''"
|
||||
- 3--*
|
||||
|
||||
- name: "-c modeonly one --"
|
||||
args:
|
||||
- "-c"
|
||||
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
|
||||
- --
|
||||
|
||||
- name: "script arguments without --"
|
||||
args:
|
||||
- script.sh
|
||||
- -1
|
||||
- -2
|
||||
- -3
|
||||
|
||||
- name: "script arguments with --"
|
||||
args:
|
||||
- script.sh
|
||||
- --
|
||||
- --1
|
||||
- -2
|
||||
- 3
|
||||
|
||||
- name: "script -- torture"
|
||||
args:
|
||||
- script.sh
|
||||
- --
|
||||
- "--"
|
||||
- --
|
||||
- -!-1*
|
||||
- "\"-2\""
|
||||
- --
|
||||
- 3--
|
||||
|
||||
- name: "script only one --"
|
||||
args:
|
||||
- script.sh
|
||||
- --
|
||||
|
||||
- name: "-c with -- separator and command"
|
||||
args:
|
||||
- "-c"
|
||||
- "--"
|
||||
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
|
||||
- "1"
|
||||
- "-2"
|
||||
- "3"
|
||||
|
||||
- name: "-c with -- separator only"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- "--"
|
||||
|
||||
- name: "-c with -- separator where command is --"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- "--"
|
||||
- "--"
|
||||
- "echo"
|
||||
- "hi"
|
||||
|
||||
- name: "-ec with -- separator and command"
|
||||
args:
|
||||
- "-ec"
|
||||
- "--"
|
||||
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
|
||||
- "1"
|
||||
- "-2"
|
||||
- "3"
|
||||
|
||||
- name: "-o with -- is not an option terminator"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-o"
|
||||
- "--"
|
||||
@@ -0,0 +1,362 @@
|
||||
name: "Arithmetic"
|
||||
cases:
|
||||
- name: "Empty expression"
|
||||
stdin: |
|
||||
echo $(())
|
||||
|
||||
- name: "Unquoted arithmetic"
|
||||
stdin: |
|
||||
echo $((1+1))
|
||||
|
||||
- name: "Arithmetic literals"
|
||||
stdin: |
|
||||
echo "$((0))"
|
||||
echo "$((1))"
|
||||
echo "$((10))"
|
||||
echo "$((010))"
|
||||
echo "$((0010))"
|
||||
echo "$((0x10))"
|
||||
echo "$((0x010))"
|
||||
|
||||
- name: "Arithmetic literals with explicit bases"
|
||||
stdin: |
|
||||
echo "$((2#1010))"
|
||||
|
||||
echo "$((8#16))"
|
||||
|
||||
echo "$((16#faf))"
|
||||
echo "$((16#FAF))"
|
||||
|
||||
echo "$((36#vvv))"
|
||||
echo "$((36#VVV))"
|
||||
|
||||
# Base 37: A-Z map to 36-61
|
||||
echo "$((37#a))"
|
||||
echo "$((37#a0))"
|
||||
echo "$((37#A))"
|
||||
|
||||
# Base 62: a-z=10-35, A-Z=36-61
|
||||
echo "$((62#A))"
|
||||
echo "$((62#a))"
|
||||
echo "$((62#Z))"
|
||||
echo "$((62#z))"
|
||||
|
||||
# Base 63: @ = 62
|
||||
echo "$((63#@))"
|
||||
echo "$((63#@9))"
|
||||
|
||||
# Base 64: _ = 63
|
||||
echo "$((64#_))"
|
||||
echo "$((64#@))"
|
||||
echo "$((64#_1))"
|
||||
echo "$((64#zzz))"
|
||||
|
||||
- name: "Unary arithmetic operators with space"
|
||||
stdin: |
|
||||
echo "$(( - 1 ))"
|
||||
echo "$(( + 1 ))"
|
||||
echo "$(( ~ 10 ))"
|
||||
echo "$(( ! 10 ))"
|
||||
|
||||
- name: "Parentheses"
|
||||
stdin: |
|
||||
echo "$(((10)))"
|
||||
echo "$((((10))))"
|
||||
echo "$(((((10)))))"
|
||||
|
||||
echo "$(( (10) ))"
|
||||
echo "$(( (1+2) ))"
|
||||
echo "$(( (1 + 2) ))"
|
||||
|
||||
echo "$(( ((1+2)) ))"
|
||||
echo "$(( ((1 + 2)) ))"
|
||||
|
||||
echo "$(((1+2)*3))"
|
||||
echo "$(((1 + 2) * 3))"
|
||||
echo "$(( (1 + 2) * 3 ))"
|
||||
|
||||
echo "$((((1+2)*3)))"
|
||||
echo "$(( ((1 + 2) * 3) ))"
|
||||
echo "$(( ( (1 + 2) * 3 ) ))"
|
||||
|
||||
- name: "Basic quoted arithmetic"
|
||||
stdin: |
|
||||
echo " 1 + 1 == $((1+1))"
|
||||
echo " 2 * 3 == $((2*3))"
|
||||
echo " 9 / 3 == $((9/3))"
|
||||
echo "10 % 3 == $((10%3))"
|
||||
echo " 2 ** 3 == $((2**3))"
|
||||
echo " 2 | 4 == $((2|4))"
|
||||
echo " 2 & 4 == $((2&4))"
|
||||
echo " 2 ^ 3 == $((2^3))"
|
||||
echo " 1 && 1 == $((1&&1))"
|
||||
echo " 1 && 0 == $((1&&0))"
|
||||
echo " 1 || 0 == $((1||0))"
|
||||
echo " 0 || 1 == $((1||0))"
|
||||
echo " 1 , 2 == $((1,2))"
|
||||
|
||||
- name: "Unary operators"
|
||||
stdin: |
|
||||
echo " -1 == $((-1))"
|
||||
echo " +1 == $((+1))"
|
||||
echo " ~10 == $((~10))"
|
||||
echo " !10 == $((!10))"
|
||||
|
||||
- name: "Conditional operator"
|
||||
stdin: |
|
||||
echo "1 ? 2 : 3 == $((1?2:3))"
|
||||
echo "0 ? 2 : 3 == $((0?2:3))"
|
||||
|
||||
- name: "Arithmetic with spacing"
|
||||
stdin: |
|
||||
echo $(( 75 + 68 ))
|
||||
|
||||
- name: "Divide by zero"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "1 / 0 == $((1/0))"
|
||||
echo "Result: $?"
|
||||
|
||||
echo "1 % 0 == $((1%0))"
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Shift arithmetic"
|
||||
stdin: |
|
||||
echo "32 >> 2 == $((32>>2))"
|
||||
echo " 1 << 4 == $((1<<4))"
|
||||
|
||||
- name: Shift arithmetic in conditional"
|
||||
stdin: |
|
||||
(( 1 >> 1 )) && echo "1. true"
|
||||
(( 1 << 1 )) && echo "2. true"
|
||||
|
||||
- name: "Variable references"
|
||||
stdin: |
|
||||
x=10
|
||||
echo "x => $((x))"
|
||||
echo "x + 1 => $((x+1))"
|
||||
|
||||
y=
|
||||
echo "y => $((y))"
|
||||
echo "y + 1 => $((y+1))"
|
||||
|
||||
z=notnumber
|
||||
echo "z => $((z))"
|
||||
echo "z + 1 => $((z+1))"
|
||||
|
||||
- name: "Complex variable references"
|
||||
stdin: |
|
||||
x=10
|
||||
y=20
|
||||
z=x+y
|
||||
a=z+z
|
||||
|
||||
echo "z => $((z))"
|
||||
echo "a => $((a))"
|
||||
|
||||
- name: "Nested expressions"
|
||||
stdin: |
|
||||
echo "1: $(($(echo -n 1; echo 2) + 37))"
|
||||
|
||||
op="+"
|
||||
echo "2: $((10 ${op} 20))"
|
||||
|
||||
expr="13 * 7"
|
||||
echo "3: $(($expr))"
|
||||
|
||||
- name: "Assignment arithmetic"
|
||||
stdin: |
|
||||
x=0
|
||||
|
||||
echo "x = 1 => $((x = 1))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "x += 3 => $((x += 3))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "x -= 1 => $((x -= 1))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "x *= 2 => $((x *= 2))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "x++ == $((x++))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "++x == $((++x))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "x-- == $((x--))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "--x == $((--x))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "x |= 3 => $((x |= 3))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "x &= 12 => $((x &= 12))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "x %= 8 => $((x %= 8))"
|
||||
echo "x is now $x"
|
||||
|
||||
echo "x ^= 7 => $((x ^= 7))"
|
||||
echo "x is now $x"
|
||||
|
||||
- name: "Assignment with references"
|
||||
stdin: |
|
||||
x=10+3
|
||||
echo "y = x => $((y = x))"
|
||||
echo "y is now $y"
|
||||
|
||||
- name: "Assignments in logical boolean expressions"
|
||||
stdin: |
|
||||
x=0
|
||||
echo "0 && x+=1 => $(( 0 && (x+=1) ))"
|
||||
echo "x: $x"
|
||||
|
||||
x=0
|
||||
echo "1 && x+=1 => $(( 1 && (x+=1) ))"
|
||||
echo "x: $x"
|
||||
|
||||
x=0
|
||||
echo "0 || x+=1 => $(( 0 || (x+=1) ))"
|
||||
echo "x: $x"
|
||||
|
||||
x=0
|
||||
echo "1 || x+=1 => $(( 1 || (x+=1) ))"
|
||||
echo "x: $x"
|
||||
|
||||
- name: "Array arithmetic"
|
||||
stdin: |
|
||||
a=(1 2 3)
|
||||
declare -p a
|
||||
echo "a[0]: $((a[0]))"
|
||||
echo "a[2]: $((a[2]))"
|
||||
echo "a[3]: $((a[3]))"
|
||||
|
||||
echo "b[2]: $((b[2]))"
|
||||
|
||||
echo "a[1]=4 => $((a[1]=4))"
|
||||
echo "a[1]: $((a[1]))"
|
||||
|
||||
echo "a[1]+=1 => $((a[1]+=1))"
|
||||
echo "a[1]: $((a[1]))"
|
||||
|
||||
echo "c[1] += 3 => $((c[1] += 3))"
|
||||
|
||||
- name: "Basic arithmetic comparison"
|
||||
stdin: |
|
||||
echo "0 < 1: $((0 < 1))"
|
||||
echo "0 <= 1: $((0 <= 1))"
|
||||
echo "0 == 1: $((0 == 1))"
|
||||
echo "0 != 1: $((0 != 1))"
|
||||
echo "0 > 1: $((0 > 1))"
|
||||
echo "0 >= 1: $((0 >= 1))"
|
||||
echo "0 < 0: $((0 < 0))"
|
||||
echo "0 <= 0: $((0 <= 0))"
|
||||
echo "0 == 0: $((0 == 0))"
|
||||
echo "0 != 0: $((0 != 0))"
|
||||
echo "0 > 0: $((0 > 0))"
|
||||
echo "0 >= 0: $((0 >= 0))"
|
||||
echo "1 < 0: $((1 < 0))"
|
||||
echo "1 <= 0: $((1 <= 0))"
|
||||
echo "1 == 0: $((1 == 0))"
|
||||
echo "1 != 0: $((1 != 0))"
|
||||
echo "1 > 0: $((1 > 0))"
|
||||
echo "1 >= 0: $((1 >= 0))"
|
||||
|
||||
- name: "Legacy bracket arithmetic syntax"
|
||||
stdin: |
|
||||
x=5
|
||||
x=$[x + 1]
|
||||
echo "x after increment: $x"
|
||||
echo "Direct: $[1 + 2]"
|
||||
|
||||
- name: "Legacy bracket arithmetic in extended test"
|
||||
stdin: |
|
||||
number=4
|
||||
[[ $[number % 2] == 0 ]] && echo "even" || echo "odd"
|
||||
number=5
|
||||
[[ $[number % 2] == 0 ]] && echo "even" || echo "odd"
|
||||
|
||||
- name: "Legacy bracket arithmetic in double-quoted string"
|
||||
stdin: |
|
||||
echo "Quoted: result: $[2 * 3]"
|
||||
|
||||
- name: "Legacy bracket arithmetic complex expression"
|
||||
stdin: |
|
||||
echo "Complex: $[1 + 2 * 3 - 4 / 2 % 2]"
|
||||
|
||||
- name: "Legacy bracket arithmetic with array subscript"
|
||||
stdin: |
|
||||
arr=(10 20 30)
|
||||
echo "Array elem: $[arr[1] + 5]"
|
||||
|
||||
- name: "Prefix increment/decrement with space before lvalue"
|
||||
stdin: |
|
||||
x=5
|
||||
echo "++ x => $(( ++ x ))"
|
||||
echo "x is now $x"
|
||||
echo "-- x => $(( -- x ))"
|
||||
echo "x is now $x"
|
||||
|
||||
# Also test in (( )) command form
|
||||
y=10
|
||||
(( ++ y ))
|
||||
echo "y after ++ y: $y"
|
||||
(( -- y ))
|
||||
echo "y after -- y: $y"
|
||||
|
||||
- name: "INT64_MIN and wrapping arithmetic"
|
||||
stdin: |
|
||||
# INT64_MIN via unary minus of the literal
|
||||
echo "$(( -9223372036854775808 ))"
|
||||
|
||||
# Wrapping negation of INT64_MIN
|
||||
echo "$(( - (-9223372036854775808) ))"
|
||||
|
||||
# INT64_MIN % -1 should be 0 (not crash)
|
||||
echo "$(( (-9223372036854775808) % (-1) ))"
|
||||
|
||||
# INT64_MIN / -1 wraps
|
||||
echo "$(( (-9223372036854775808) / (-1) ))"
|
||||
|
||||
# Wrapping increment/decrement near boundaries
|
||||
echo "$(( 9223372036854775807 + 1 ))" # wraps to INT64_MIN
|
||||
|
||||
- name: "Non-cyclic variable dereference chain"
|
||||
stdin: |
|
||||
a=b
|
||||
b=c
|
||||
c=42
|
||||
echo "$((a))"
|
||||
|
||||
- name: "Self-referencing variable in arithmetic"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
a=a
|
||||
echo "$(( a ))"
|
||||
echo "exit: $?"
|
||||
|
||||
- name: "Recursive variable evaluation with depth limit"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
# Recursive variables should not cause stack overflow
|
||||
a=b
|
||||
b=a
|
||||
echo "$(( a ))"
|
||||
echo "exit: $?"
|
||||
|
||||
- name: "Arithmetic with nameref variable"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
counter=10
|
||||
declare -n ref=counter
|
||||
echo "before: $counter"
|
||||
(( ref++ ))
|
||||
echo "after ++: $counter"
|
||||
(( ref += 5 ))
|
||||
echo "after +=5: $counter"
|
||||
echo "expr: $(( ref * 2 ))"
|
||||
@@ -0,0 +1,363 @@
|
||||
name: "Arrays"
|
||||
incompatible_configs: ["sh"]
|
||||
common_test_files:
|
||||
- path: "helpers.sh"
|
||||
contents: |
|
||||
stable_print_assoc_array() {
|
||||
# TODO(nameref): enable use of nameref when implemented; for now
|
||||
# we assume the name of the array is assoc_array
|
||||
# local -n assoc_array=$1
|
||||
local key
|
||||
|
||||
for key in $(printf "%s\n" "${!assoc_array[@]}" | sort -n); do
|
||||
echo "\"${key}\" => ${assoc_array[${key}]}"
|
||||
done
|
||||
}
|
||||
cases:
|
||||
- name: "Basic indexed array definition and access"
|
||||
stdin: |
|
||||
var=(a b c)
|
||||
echo "var: $var"
|
||||
echo "var: ${var}"
|
||||
echo "var[0]: ${var[0]}"
|
||||
echo "var[1]: ${var[1]}"
|
||||
echo "var[2]: ${var[2]}"
|
||||
echo "var[3]: ${var[3]}"
|
||||
echo "var[4]: ${var[4]}"
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Negative indexed array access"
|
||||
stdin: |
|
||||
var=(a b c)
|
||||
echo "var[-1]: ${var[-1]}"
|
||||
echo "var[-2]: ${var[-2]}"
|
||||
echo "var[-3]: ${var[-3]}"
|
||||
|
||||
- name: "Too large negative indexed array access"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
var=(a b c)
|
||||
echo "var[-4]: ${var[-4]}"
|
||||
|
||||
- name: "Negative indexed array access (set)"
|
||||
stdin: |
|
||||
var=(a b c d e f g)
|
||||
var[-1]=x
|
||||
var[-3]=z
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Too large negative indexed array access (set)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
var=(a b c)
|
||||
var[-4]=x
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Define array with expansion"
|
||||
stdin: |
|
||||
body='a b'
|
||||
|
||||
var=($body)
|
||||
declare -p var
|
||||
|
||||
declare -a var2=($body)
|
||||
declare -p var2
|
||||
|
||||
- name: "Arrays with empty values"
|
||||
stdin: |
|
||||
var1=(a "" c)
|
||||
declare -p var1
|
||||
|
||||
var2=(a "$nonexistent" c)
|
||||
declare -p var2
|
||||
|
||||
var3=("" "" "")
|
||||
declare -p var3
|
||||
|
||||
- name: "Basic associative array definition and access"
|
||||
stdin: |
|
||||
declare -A var=([0]=99 [a]=3 [b]=4 [c]=5)
|
||||
echo "var: $var"
|
||||
echo "var: ${var}"
|
||||
echo "var[0]: ${var[0]}"
|
||||
echo "var[a]: ${var[a]}"
|
||||
echo "var[b]: ${var[b]}"
|
||||
echo "var[c]: ${var[c]}"
|
||||
echo "var[d]: ${var[d]}"
|
||||
|
||||
key="a"
|
||||
echo "var[key]: ${var[${key}]}"
|
||||
|
||||
- name: "Array updating via index"
|
||||
stdin: |
|
||||
var=(a b c)
|
||||
var[1]="d"
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Array index assignment"
|
||||
stdin: |
|
||||
x=(3 2 1)
|
||||
y[${x[0]}]=10
|
||||
y[x[1]]=11
|
||||
declare -p y
|
||||
|
||||
- name: "Replacing array with string"
|
||||
stdin: |
|
||||
var=(a b c)
|
||||
var="x"
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Array replacing"
|
||||
stdin: |
|
||||
var=(a b c)
|
||||
var=(d e f)
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Appending array to indexed array"
|
||||
stdin: |
|
||||
var=(a b c)
|
||||
var+=(e f g)
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
var+=("" "" "")
|
||||
declare -p var
|
||||
|
||||
var+=(h "i" "$nonexistent" j)
|
||||
var+=(j k l)
|
||||
declare -p var
|
||||
|
||||
- name: "Appending array to declared-but-unset indexed array"
|
||||
stdin: |
|
||||
declare -a var
|
||||
var+=(e f g)
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Appending item to indexed array"
|
||||
stdin: |
|
||||
var=(a b c)
|
||||
var+=d
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Appending item to element of indexed array"
|
||||
stdin: |
|
||||
declare -a var=(1 2 3)
|
||||
var[1]+=2
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Appending item to element of int indexed array"
|
||||
stdin: |
|
||||
declare -ai var=(1 2 3)
|
||||
var[1]+=2
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Appending items to int indexed array"
|
||||
stdin: |
|
||||
declare -ai var=(1 2 3)
|
||||
var+=(4 value 6)
|
||||
declare -p var
|
||||
|
||||
- name: "Appending array to associative array"
|
||||
stdin: |
|
||||
source helpers.sh
|
||||
declare -A assoc_array=([a]=1 [b]=2 [c]=3)
|
||||
assoc_array+=([d]=4 [e]=5 [f]=6)
|
||||
stable_print_assoc_array assoc_array
|
||||
|
||||
- name: "Appending array to declared-but-unset associative array"
|
||||
stdin: |
|
||||
source helpers.sh
|
||||
declare -A assoc_array
|
||||
assoc_array+=([d]=4 [e]=5 [f]=6)
|
||||
stable_print_assoc_array assoc_array
|
||||
|
||||
- name: "Appending item to associative array"
|
||||
stdin: |
|
||||
source helpers.sh
|
||||
declare -A assoc_array=([a]=1 [b]=2 [c]=3)
|
||||
assoc_array+=d
|
||||
stable_print_assoc_array assoc_array
|
||||
|
||||
- name: "Appending item to element of associative array"
|
||||
stdin: |
|
||||
source helpers.sh
|
||||
declare -A assoc_array=([a]=1 [b]=2 [c]=3)
|
||||
assoc_array[b]+=2
|
||||
stable_print_assoc_array assoc_array
|
||||
|
||||
- name: "Appending item to element of int associative array"
|
||||
stdin: |
|
||||
source helpers.sh
|
||||
declare -Ai assoc_array=([a]=1 [b]=2 [c]=3)
|
||||
assoc_array[b]+=2
|
||||
stable_print_assoc_array assoc_array
|
||||
|
||||
- name: "Appending items to int associative array"
|
||||
stdin: |
|
||||
source helpers.sh
|
||||
declare -Ai assoc_array=([a]=1 [b]=2 [c]=3)
|
||||
assoc_array+=([d]=4 [e]=value [f]=6)
|
||||
stable_print_assoc_array assoc_array
|
||||
|
||||
- name: "Appending array to item"
|
||||
stdin: |
|
||||
var=x
|
||||
var+=(a b c)
|
||||
echo "var[*]: ${var[*]}"
|
||||
declare -p var
|
||||
|
||||
- name: "Fill associative array"
|
||||
stdin: |
|
||||
declare -Ag myarray
|
||||
|
||||
myfunc() {
|
||||
local myarg=$1 element
|
||||
shift
|
||||
for element in "$@"; do
|
||||
echo "Setting: myarray[$element]=$myarg"
|
||||
myarray[$element]="${myarg}${element}"
|
||||
done
|
||||
}
|
||||
|
||||
myfunc 'first' bunzip2 bzcat pbunzip2 pbzcat lbunzip2 lbzcat
|
||||
|
||||
- name: "Declare via array index"
|
||||
stdin: |
|
||||
declare -A assoc_array["key"]="value"
|
||||
declare -p assoc_array
|
||||
|
||||
declare -a indexed_array[5]="value"
|
||||
declare -p indexed_array
|
||||
|
||||
declare implicit_array[5]="value"
|
||||
declare -p implicit_array
|
||||
|
||||
declare -A no_value_assoc_array["key"]
|
||||
declare -p no_value_assoc_array
|
||||
|
||||
declare no_value_indexed_array[33]
|
||||
declare -p no_value_indexed_array
|
||||
|
||||
- name: "Create array via index update"
|
||||
stdin: |
|
||||
i=0
|
||||
my_array[i++]="c"
|
||||
echo "0: i=$i array=${my_array[@]}"
|
||||
my_array[i++]="b"
|
||||
echo "1: i=$i array=${my_array[@]}"
|
||||
my_array[i++]="a"
|
||||
echo "2: i=$i array=${my_array[@]}"
|
||||
|
||||
- name: "Expansion of array body"
|
||||
stdin: |
|
||||
elements="a b c"
|
||||
|
||||
array1=(${elements})
|
||||
declare -p array1
|
||||
|
||||
array2=("${elements}")
|
||||
declare -p array2
|
||||
|
||||
- name: "Copy array"
|
||||
stdin: |
|
||||
src=(a b c)
|
||||
|
||||
dest1=("${src[*]}")
|
||||
declare -p dest1
|
||||
|
||||
dest2=(${src[*]})
|
||||
declare -p dest2
|
||||
|
||||
dest3=("${src[@]}")
|
||||
declare -p dest3
|
||||
|
||||
dest4=(${src[@]})
|
||||
declare -p dest4
|
||||
|
||||
- name: "Multi-line definition of array"
|
||||
stdin: |
|
||||
array=(
|
||||
a
|
||||
b
|
||||
c
|
||||
)
|
||||
declare -p array
|
||||
|
||||
- name: "Dump values from non-existent array"
|
||||
stdin: |
|
||||
for key in ${NONEXISTENT[@]}; do
|
||||
echo "@-Key: ${key}"
|
||||
done
|
||||
for key in "${NONEXISTENT[@]}"; do
|
||||
echo "@-Quoted Key: ${key}"
|
||||
done
|
||||
|
||||
for key in ${NONEXISTENT[*]}; do
|
||||
echo "*-Key: ${key}"
|
||||
done
|
||||
for key in "${NONEXISTENT[*]}"; do
|
||||
echo "*-Quoted Key: ${key}"
|
||||
done
|
||||
|
||||
- name: "Use non-existent var in indexed array index"
|
||||
stdin: |
|
||||
declare -a myarray=(10)
|
||||
echo "Value: ${myarray[$non_existent_var]}"
|
||||
|
||||
- name: "Use non-existent var in associative array index"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
declare -A myarray=([0]="default" ["a"]="other")
|
||||
echo "Value: ${myarray[$non_existent_var]}"
|
||||
|
||||
- name: "Nested array reference"
|
||||
stdin: |
|
||||
x=(2 3 4)
|
||||
echo ${x[${x[0]}]}
|
||||
|
||||
- name: "Append array-resulting expansion to array"
|
||||
stdin: |
|
||||
declare -a otherarray=("a" "b" "c")
|
||||
declare -a ourarray=()
|
||||
|
||||
ourarray+=("${otherarray[@]/a/x}")
|
||||
declare -p otherarray
|
||||
declare -p ourarray
|
||||
|
||||
- name: "Array declaration in a function"
|
||||
stdin: |
|
||||
outer() {
|
||||
inner=()
|
||||
if test true; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
}
|
||||
|
||||
outer
|
||||
|
||||
- name: "Array element expansion with non-standard IFS"
|
||||
stdin: |
|
||||
IFS='|'
|
||||
|
||||
declare -a myarray=(abc 1 f)
|
||||
echo "\"\${myarray[@]}\": ${myarray[@]}"
|
||||
echo "\"\${myarray[*]}\": ${myarray[*]}"
|
||||
echo "\${myarray[@]}:" ${myarray[@]}
|
||||
echo "\${myarray[*]}:" ${myarray[*]}
|
||||
|
||||
- name: "Empty array with newline"
|
||||
stdin: |
|
||||
declare -a myarray=(
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
name: "Assignments"
|
||||
cases:
|
||||
- name: "First char is equals sign"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
=x
|
||||
|
||||
- name: "Basic assignment"
|
||||
stdin: |
|
||||
x=yz
|
||||
echo "x: ${x}"
|
||||
|
||||
- name: "Invalid variable name"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
@=something
|
||||
|
||||
- name: "Quoted equals sign"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
x"="3
|
||||
|
||||
- name: "Multiple equals signs"
|
||||
stdin: |
|
||||
x=y=z
|
||||
echo "x: ${x}"
|
||||
|
||||
- name: "Assignment with tilde expansion"
|
||||
env:
|
||||
HOME: /some/dir
|
||||
stdin: |
|
||||
# Basic tilde at start (currently works)
|
||||
var=~/file1.txt
|
||||
echo "~/file1.txt: ${var}"
|
||||
|
||||
# Tilde after colon in assignment (currently broken - doesn't expand second tilde)
|
||||
var=~/file1.txt:~/file2.txt
|
||||
echo "~/file1.txt:~/file2.txt: ${var}"
|
||||
|
||||
# Multiple tildes after colons
|
||||
var=~:~/bin:~/local/bin
|
||||
echo "~:~/bin:~/local/bin: ${var}"
|
||||
|
||||
# Mixed with regular paths
|
||||
PATH=/usr/bin:~:/bin:~/local
|
||||
echo "PATH-like: ${PATH}"
|
||||
|
||||
- name: "Array assignment with tilde expansion"
|
||||
env:
|
||||
HOME: /testhome
|
||||
stdin: |
|
||||
# Array elements with tildes
|
||||
arr=(~ ~/bin ~root)
|
||||
echo "Array elements:"
|
||||
for i in "${arr[@]}"; do
|
||||
echo " $i"
|
||||
done
|
||||
|
||||
# Array elements with colon-tilde
|
||||
arr2=(~:~/bin /usr/bin:~)
|
||||
echo "Array with colons:"
|
||||
for i in "${arr2[@]}"; do
|
||||
echo " $i"
|
||||
done
|
||||
|
||||
- name: "Append assignment with tilde expansion"
|
||||
env:
|
||||
HOME: /testhome
|
||||
stdin: |
|
||||
# Basic append
|
||||
PATH=/usr/bin
|
||||
PATH+=:~
|
||||
echo "After append tilde: ${PATH}"
|
||||
|
||||
# Append with colon-tilde
|
||||
PATH=/usr/bin
|
||||
PATH+=:~:~/local
|
||||
echo "After append colon-tilde: ${PATH}"
|
||||
@@ -0,0 +1,89 @@
|
||||
name: "Background jobs"
|
||||
cases:
|
||||
# === Implicit wait on exit: shell must wait for bg jobs before terminating ===
|
||||
# These use file side-effects rather than stdout to avoid racy output ordering.
|
||||
|
||||
- name: "Single background job via -c without explicit wait"
|
||||
skip: true # Issue #1079
|
||||
args:
|
||||
- "-c"
|
||||
- "touch bg_done &"
|
||||
|
||||
- name: "Single background job via stdin without explicit wait"
|
||||
skip: true # Issue #1079
|
||||
stdin: |
|
||||
touch bg_done &
|
||||
|
||||
- name: "Background compound command via -c without explicit wait"
|
||||
skip: true # Issue #1079
|
||||
args:
|
||||
- "-c"
|
||||
- "{ touch a_done; touch b_done; } &"
|
||||
|
||||
- name: "Background compound command via stdin without explicit wait"
|
||||
skip: true # Issue #1079
|
||||
stdin: |
|
||||
{ touch a_done; touch b_done; } &
|
||||
|
||||
- name: "Multiple background jobs via -c without explicit wait"
|
||||
skip: true # Issue #1079
|
||||
args:
|
||||
- "-c"
|
||||
- "touch one & touch two & touch three &"
|
||||
|
||||
- name: "Multiple background jobs via stdin without explicit wait"
|
||||
skip: true # Issue #1079
|
||||
stdin: |
|
||||
touch one & touch two & touch three &
|
||||
|
||||
- name: "Background and foreground via -c without explicit wait"
|
||||
skip: true # Issue #1079
|
||||
args:
|
||||
- "-c"
|
||||
- "touch bg_done & touch fg_done"
|
||||
|
||||
- name: "Background and foreground via stdin without explicit wait"
|
||||
skip: true # Issue #1079
|
||||
stdin: |
|
||||
touch bg_done & touch fg_done
|
||||
|
||||
# === Explicit wait ===
|
||||
|
||||
- name: "Background job side effect observed after wait via -c"
|
||||
args:
|
||||
- "-c"
|
||||
- "touch bg_done & wait; test -f bg_done && echo exists"
|
||||
|
||||
- name: "Background job side effect observed after wait via stdin"
|
||||
stdin: |
|
||||
touch bg_done &
|
||||
wait
|
||||
test -f bg_done && echo exists
|
||||
|
||||
- name: "Single background job with explicit wait via -c"
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
echo hello &
|
||||
wait
|
||||
|
||||
- name: "Single background job with explicit wait via stdin"
|
||||
stdin: |
|
||||
echo hello &
|
||||
wait
|
||||
|
||||
- name: "Two sequential background jobs with explicit wait via -c"
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
echo 1 &
|
||||
wait
|
||||
echo 2 &
|
||||
wait
|
||||
|
||||
- name: "Two sequential background jobs with explicit wait via stdin"
|
||||
stdin: |
|
||||
echo 1 &
|
||||
wait
|
||||
echo 2 &
|
||||
wait
|
||||
@@ -0,0 +1,132 @@
|
||||
name: "Basic tests"
|
||||
cases:
|
||||
- name: "Basic -c usage"
|
||||
args:
|
||||
- "-c"
|
||||
- "echo hi"
|
||||
|
||||
- name: "Basic -c usage: output"
|
||||
stdin: |
|
||||
# Capture raw output of command
|
||||
raw_output=$($0 -c 'echo hi')
|
||||
|
||||
# Dump raw output as characters
|
||||
echo "[Character output]"
|
||||
for ((i = 0; i < ${#raw_output}; i++)); do
|
||||
char="${raw_output:i:1}"
|
||||
printf '%c' "$char"
|
||||
done
|
||||
printf '\n\n'
|
||||
|
||||
# Dump raw output as hex
|
||||
echo "[Hex output]"
|
||||
for ((i = 0; i < ${#raw_output}; i++)); do
|
||||
if (( i % 16 == 0 )); then
|
||||
printf '%04x: ' "$i"
|
||||
fi
|
||||
|
||||
char="${raw_output:i:1}"
|
||||
printf '%02x ' "'$char"
|
||||
|
||||
if (( (i + 1) % 16 == 0 )); then
|
||||
printf '\n'
|
||||
fi
|
||||
done
|
||||
|
||||
printf '\n'
|
||||
|
||||
- name: "Basic stdin usage"
|
||||
stdin: |
|
||||
echo hi
|
||||
|
||||
- name: "Basic sequence"
|
||||
stdin: |
|
||||
echo 'hi'; echo 'there'
|
||||
|
||||
- name: "Basic -s usage with -c"
|
||||
args: ["-s", "-c", "echo hi1 hi2"]
|
||||
stdin: |
|
||||
[[ $(type -P $0) == $(type -P $BASH) ]] && echo '$0 matches $BASH'
|
||||
echo "\$1: $1"
|
||||
echo "\$2: $2"
|
||||
echo "\$3: $3"
|
||||
|
||||
- name: "Basic -s usage with positional args"
|
||||
args: ["-s", "arg1", "arg2", "arg3"]
|
||||
stdin: |
|
||||
[[ $(type -P $0) == $(type -P $BASH) ]] && echo '$0 matches $BASH'
|
||||
echo "\$1: $1"
|
||||
echo "\$2: $2"
|
||||
echo "\$3: $3"
|
||||
|
||||
- name: "Basic -i usage with commands via stdin"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo hi
|
||||
echo there
|
||||
|
||||
- name: "Basic script execution"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "ARGS: $@"
|
||||
exit 22
|
||||
args: ["./script.sh", 1, 2, 3]
|
||||
|
||||
- name: "Passing option-like arg to script"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "ARGS: $@"
|
||||
exit 22
|
||||
args: ["./script.sh", "-v"]
|
||||
|
||||
- name: "Ensure ~ is resolvable"
|
||||
stdin: "test ~"
|
||||
|
||||
- name: "Rc file loading (interactive)"
|
||||
ignore_stderr: true
|
||||
removed_default_args: ["--norc"] # Need to cancel this out so we can override
|
||||
args: ["--rcfile", "testrc", "-i"]
|
||||
env:
|
||||
PS1: ""
|
||||
test_files:
|
||||
- path: "testrc"
|
||||
contents: |
|
||||
echo "Executing in rc file"
|
||||
stdin: |
|
||||
echo "Executing in main script body"
|
||||
|
||||
- name: "Rc file loading (non-interactive)"
|
||||
ignore_stderr: true
|
||||
removed_default_args: ["--norc"] # Need to cancel this out so we can override
|
||||
args: ["--rcfile", "testrc"]
|
||||
env:
|
||||
PS1: ""
|
||||
test_files:
|
||||
- path: "testrc"
|
||||
contents: |
|
||||
echo "Executing in rc file"
|
||||
stdin: |
|
||||
echo "Executing in main script body"
|
||||
|
||||
- name: "Missing rc file"
|
||||
ignore_stderr: true
|
||||
removed_default_args: ["--norc"] # Need to cancel this out so we can override
|
||||
args: ["--rcfile", "non-existent-file", "-i"]
|
||||
env:
|
||||
PS1: ""
|
||||
stdin: |
|
||||
echo "Executing in main script body"
|
||||
|
||||
- name: "Unknown option"
|
||||
ignore_stderr: true
|
||||
args: ["--unknown-option", "-c", "echo hi"]
|
||||
|
||||
- name: "-f on the command line"
|
||||
args: ["-f"]
|
||||
stdin: |
|
||||
touch abc
|
||||
touch abd
|
||||
echo a*
|
||||
@@ -0,0 +1,33 @@
|
||||
name: "Builtins: alias"
|
||||
cases:
|
||||
- name: "Basic alias usage"
|
||||
stdin: |
|
||||
shopt -s expand_aliases
|
||||
alias myalias=echo
|
||||
alias
|
||||
myalias 'hello'
|
||||
|
||||
- name: "Alias with trailing space"
|
||||
known_failure: true # Issue #57
|
||||
stdin: |
|
||||
shopt -s expand_aliases
|
||||
alias cmd='echo '
|
||||
alias other='replaced '
|
||||
alias otherother='also-replaced'
|
||||
|
||||
cmd other otherother
|
||||
|
||||
- name: "Alias referencing to alias"
|
||||
known_failure: true # Issue #57
|
||||
stdin: |
|
||||
shopt -s expand_aliases
|
||||
alias myalias=echo
|
||||
alias outeralias=myalias
|
||||
outeralias 'hello'
|
||||
|
||||
- name: "Alias to keywords"
|
||||
known_failure: true # Issue #286
|
||||
stdin: |
|
||||
shopt -s expand_aliases
|
||||
alias myalias=if
|
||||
myalias true; then echo "true"; fi
|
||||
@@ -0,0 +1,160 @@
|
||||
name: "Builtins: bind"
|
||||
cases:
|
||||
- name: "bind succeeds in non-interactive mode"
|
||||
stdin: |
|
||||
bind > /dev/null 2>&1
|
||||
echo "exit: $?"
|
||||
|
||||
- name: "bind -l"
|
||||
args: ["run.sh"]
|
||||
removed_default_args: ["--input-backend=basic"]
|
||||
additional_test_args: ["--input-backend=reedline"]
|
||||
ignore_stderr: true
|
||||
min_oracle_version: "5.3" # Added functions in 5.3
|
||||
test_files:
|
||||
- path: "run.sh"
|
||||
contents: |
|
||||
# List bindable actions but ignore brush custom entries
|
||||
echo "[bind -l]"
|
||||
bind -l | grep -v brush- | sort
|
||||
echo "bind -l result: ${PIPESTATUS[@]}"
|
||||
|
||||
- name: "bind -q/-P/-p"
|
||||
args: ["run.sh"]
|
||||
removed_default_args: ["--input-backend=basic"]
|
||||
additional_test_args: ["--input-backend=reedline"]
|
||||
ignore_stderr: true
|
||||
test_files:
|
||||
- path: "run.sh"
|
||||
contents: |
|
||||
# Basic-check some known bindings
|
||||
well_known_func="clear-screen"
|
||||
|
||||
echo "[bind -q]"
|
||||
bind -q ${well_known_func}
|
||||
echo "bind -q result: ${PIPESTATUS[@]}"
|
||||
|
||||
echo "[bind -P]"
|
||||
bind -P | grep ${well_known_func}
|
||||
echo "bind -P result: ${PIPESTATUS[@]}"
|
||||
|
||||
echo "[bind -p]"
|
||||
bind -p | grep ${well_known_func}
|
||||
echo "bind -p result: ${PIPESTATUS[@]}"
|
||||
|
||||
- name: "bind -x/-X/-r/-u"
|
||||
args: ["run.sh"]
|
||||
removed_default_args: ["--input-backend=basic"]
|
||||
additional_test_args: ["--input-backend=reedline"]
|
||||
min_oracle_version: "5.3" # -X output changed in 5.3
|
||||
ignore_stderr: true
|
||||
test_files:
|
||||
- path: "run.sh"
|
||||
contents: |
|
||||
# Bind a key and check it
|
||||
echo "[bind -x]"
|
||||
bind -x '"\C-t":echo hi'
|
||||
echo "bind -x result: $?"
|
||||
|
||||
echo "[bind -X]"
|
||||
bind -X
|
||||
echo "bind -X result: $?"
|
||||
|
||||
# Unbind it via -r
|
||||
echo "[bind -r]"
|
||||
bind -r '\C-t'
|
||||
echo "bind -r result: $?"
|
||||
bind -X
|
||||
echo "bind -X result: $?"
|
||||
echo "[bind -r (again)]"
|
||||
bind -r '\C-t'
|
||||
echo "bind -r (again) result: $?"
|
||||
|
||||
# Unbind via -u
|
||||
well_known_func="clear-screen"
|
||||
echo "[bind -u]"
|
||||
bind -u ${well_known_func}
|
||||
echo "bind -u result: $?"
|
||||
echo "[bind -q (after -u)]"
|
||||
bind -q ${well_known_func}
|
||||
echo "bind -q (after -u) result: $?"
|
||||
|
||||
- name: "bind keyseq"
|
||||
args: ["run.sh"]
|
||||
removed_default_args: ["--input-backend=basic"]
|
||||
additional_test_args: ["--input-backend=reedline"]
|
||||
ignore_stderr: true
|
||||
test_files:
|
||||
- path: "run.sh"
|
||||
contents: |
|
||||
well_known_func="clear-screen"
|
||||
bind -u ${well_known_func} # Ensure it's unbound first
|
||||
|
||||
# Bind a keyseq
|
||||
echo "[bind keyseq]"
|
||||
bind '"\C-t":clear-screen'
|
||||
echo "bind keyseq result: $?"
|
||||
|
||||
# List current bindings
|
||||
bind -q ${well_known_func}
|
||||
echo "bind -q result: $?"
|
||||
|
||||
- name: "bind macro 1"
|
||||
args: ["run.sh"]
|
||||
removed_default_args: ["--input-backend=basic"]
|
||||
additional_test_args: ["--input-backend=reedline"]
|
||||
ignore_stderr: true
|
||||
test_files:
|
||||
- path: "run.sh"
|
||||
contents: |
|
||||
key_to_bind='\C-r'
|
||||
|
||||
echo "[bind -S (pre)]"
|
||||
bind -S | grep "${key_to_bind}"
|
||||
echo "bind -S result: ${PIPESTATUS[@]}"
|
||||
|
||||
echo "[bind -s (pre)]"
|
||||
bind -s | grep "${key_to_bind}"
|
||||
echo "bind -s result: ${PIPESTATUS[@]}"
|
||||
|
||||
echo "[bind macro]"
|
||||
bind '"\C-r": "\e[0;1A\e[0;0A"'
|
||||
echo "bind macro result: $?"
|
||||
|
||||
echo "[bind -S]"
|
||||
bind -S | grep "${key_to_bind}"
|
||||
echo "bind -S result: ${PIPESTATUS[@]}"
|
||||
|
||||
echo "[bind -s]"
|
||||
bind -s | grep "${key_to_bind}"
|
||||
echo "bind -s result: ${PIPESTATUS[@]}"
|
||||
|
||||
echo "[bind -r]"
|
||||
bind -r "${key_to_bind}"
|
||||
echo "bind -r result: ${PIPESTATUS[@]}"
|
||||
|
||||
echo "[bind -s (after -r)]"
|
||||
bind -s | grep "${key_to_bind}"
|
||||
echo "bind -s (after -r) result: ${PIPESTATUS[@]}"
|
||||
|
||||
echo "[bind -S (after -r)]"
|
||||
bind -S | grep "${key_to_bind}"
|
||||
echo "bind -S (after -r) result: ${PIPESTATUS[@]}"
|
||||
|
||||
- name: "bind macro 2"
|
||||
args: ["run.sh"]
|
||||
removed_default_args: ["--input-backend=basic"]
|
||||
additional_test_args: ["--input-backend=reedline"]
|
||||
ignore_stderr: true
|
||||
test_files:
|
||||
- path: "run.sh"
|
||||
contents: |
|
||||
key_to_bind='\ec'
|
||||
|
||||
echo "[bind macro]"
|
||||
bind '"\ec": " \C-b\C-k`something`\e\C-e"'
|
||||
echo "bind macro result: $?"
|
||||
|
||||
echo "[bind -S]"
|
||||
bind -S | grep "${key_to_bind}"
|
||||
echo "bind -S result: ${PIPESTATUS[@]}"
|
||||
@@ -0,0 +1,44 @@
|
||||
name: "Builtins: builtin"
|
||||
cases:
|
||||
- name: "builtin with no builtin"
|
||||
stdin: builtin
|
||||
|
||||
- name: "builtin with unknown builtin"
|
||||
ignore_stderr: true
|
||||
stdin: builtin not-a-builtin args
|
||||
|
||||
- name: "valid builtin"
|
||||
stdin: builtin echo "Hello world"
|
||||
|
||||
- name: "valid builtin with hyphen args"
|
||||
stdin: builtin echo -e "Hello\nWorld"
|
||||
|
||||
- name: "builtin passing through results"
|
||||
stdin: |
|
||||
builtin false; echo "builtin false => $?"
|
||||
builtin true; echo "builtin true => $?"
|
||||
|
||||
- name: "builtin with non-decl builtin"
|
||||
stdin: |
|
||||
builtin echo variable=value
|
||||
|
||||
- name: "builtin with decl builtin"
|
||||
stdin: |
|
||||
builtin declare variable=value
|
||||
declare -p variable
|
||||
|
||||
- name: "builtin with disabled builtin"
|
||||
ignore_stderr: true
|
||||
incompatible_os:
|
||||
- opensuse-tumbleweed
|
||||
stdin: |
|
||||
cd .
|
||||
echo $?
|
||||
|
||||
enable -n cd
|
||||
builtin cd .
|
||||
echo $?
|
||||
|
||||
enable cd
|
||||
builtin cd .
|
||||
echo $?
|
||||
@@ -0,0 +1,67 @@
|
||||
name: "caller builtin"
|
||||
cases:
|
||||
- name: "caller in function"
|
||||
test_files:
|
||||
- path: script.sh
|
||||
contents: |
|
||||
function outer() {
|
||||
echo "In outer"
|
||||
inner
|
||||
}
|
||||
|
||||
function inner() {
|
||||
echo "In inner"
|
||||
|
||||
echo "[Calling caller 0]"
|
||||
caller 0
|
||||
|
||||
echo "[Calling caller 1]"
|
||||
caller 1
|
||||
}
|
||||
stdin: |
|
||||
source script.sh
|
||||
outer
|
||||
|
||||
- name: "caller no args"
|
||||
known_failure: true # TODO(caller): needs investigation
|
||||
test_files:
|
||||
- path: script.sh
|
||||
contents: |
|
||||
function myfunc() {
|
||||
echo "In myfunc"
|
||||
caller
|
||||
}
|
||||
stdin: |
|
||||
source script.sh
|
||||
myfunc
|
||||
|
||||
- name: "caller no args (nested)"
|
||||
test_files:
|
||||
- path: script.sh
|
||||
contents: |
|
||||
function myfunc() {
|
||||
echo "In myfunc"
|
||||
caller
|
||||
}
|
||||
|
||||
function myouterfunc() {
|
||||
echo "In myouterfunc"
|
||||
myfunc
|
||||
}
|
||||
stdin: |
|
||||
source script.sh
|
||||
myouterfunc
|
||||
|
||||
- name: "caller invalid expr"
|
||||
ignore_stderr: true # Different error messages
|
||||
stdin: |
|
||||
f() { caller x; }
|
||||
f
|
||||
|
||||
- name: "caller out of range"
|
||||
stdin: |
|
||||
caller 100
|
||||
|
||||
- name: "caller not in function"
|
||||
stdin: |
|
||||
caller
|
||||
@@ -0,0 +1,171 @@
|
||||
name: "Builtins: cd"
|
||||
cases:
|
||||
- name: "Basic cd usage"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "cd /"
|
||||
cd /
|
||||
echo $?
|
||||
echo "pwd: $PWD"
|
||||
|
||||
echo "cd usr"
|
||||
cd usr
|
||||
echo $?
|
||||
echo "pwd: $PWD"
|
||||
|
||||
echo "cd"
|
||||
cd
|
||||
echo $?
|
||||
echo "pwd: $PWD"
|
||||
|
||||
- name: "cd to file"
|
||||
ignore_stderr: true
|
||||
test_files:
|
||||
- path: "file"
|
||||
contents: "file contents"
|
||||
stdin: |
|
||||
ls -1
|
||||
cd file
|
||||
echo $?
|
||||
ls -1
|
||||
|
||||
- name: "cd -"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
cd /
|
||||
echo "pwd: $PWD"
|
||||
cd usr
|
||||
echo "pwd: $PWD"
|
||||
echo "oldpwd: $OLDPWD"
|
||||
cd -
|
||||
echo $?
|
||||
echo "pwd: $PWD"
|
||||
|
||||
- name: "cd ~"
|
||||
stdin: |
|
||||
mkdir ./my_home
|
||||
export HOME="$(realpath ./my_home)"
|
||||
echo "Set HOME: $(basename $HOME)"
|
||||
(
|
||||
echo "Subshell 1: HOME=$(basename $HOME)"
|
||||
cd ~
|
||||
echo "pwd: $(basename $PWD)"
|
||||
)
|
||||
(
|
||||
echo "Subshell 2: HOME=$(basename $HOME)"
|
||||
cd -L ~
|
||||
echo "pwd: $(basename $PWD)"
|
||||
)
|
||||
(
|
||||
echo "Subshell 3: HOME=$(basename $HOME)"
|
||||
cd -P ~
|
||||
echo "pwd: $(basename $PWD)"
|
||||
)
|
||||
|
||||
- name: "cd with symlink"
|
||||
stdin: |
|
||||
mkdir -p ./level1/level2/level3
|
||||
cd level1
|
||||
ln -s ./level2/level3 ./symlink
|
||||
|
||||
# -L by default
|
||||
(
|
||||
cd ./symlink
|
||||
echo "$(basename $PWD)"
|
||||
cd ..
|
||||
echo "$(basename $PWD)"
|
||||
)
|
||||
|
||||
- name: "cd -L"
|
||||
stdin: |
|
||||
mkdir -p ./level1/level2/level3
|
||||
cd level1
|
||||
ln -s ./level2/level3 ./symlink
|
||||
|
||||
echo "[Case 1]"
|
||||
(
|
||||
cd ./symlink
|
||||
echo "$(basename $PWD)"
|
||||
cd -L ..
|
||||
echo "$(basename $PWD)"
|
||||
)
|
||||
|
||||
echo "[Case 2]"
|
||||
(
|
||||
cd -L ./symlink
|
||||
echo "$(basename $PWD)"
|
||||
cd ..
|
||||
echo "$(basename $PWD)"
|
||||
)
|
||||
|
||||
# without pwd
|
||||
echo "[Case 3]"
|
||||
(
|
||||
cd -L ./symlink
|
||||
echo "$(basename $PWD)"
|
||||
export PWD=
|
||||
cd -L ..
|
||||
echo "$(basename $PWD)"
|
||||
)
|
||||
|
||||
echo "[Case 4]"
|
||||
(
|
||||
cd -L ./symlink
|
||||
export PWD=
|
||||
# start a shell without $PWD
|
||||
(
|
||||
cd .
|
||||
echo "$(basename $PWD)"
|
||||
)
|
||||
)
|
||||
|
||||
- name: "cd -P"
|
||||
stdin: |
|
||||
mkdir -p ./level1/level2/level3
|
||||
cd level1
|
||||
ln -s ./level2/level3 ./symlink
|
||||
|
||||
echo "[Case 1]"
|
||||
(
|
||||
cd ./symlink
|
||||
echo "cd ./symlink => $(basename $PWD)"
|
||||
cd -P ..
|
||||
echo "cd -P .. => $(basename $PWD)"
|
||||
)
|
||||
|
||||
echo "[Case 2]"
|
||||
(
|
||||
cd -P ./symlink
|
||||
echo "cd -P ./symlink => $(basename $PWD)"
|
||||
cd ..
|
||||
echo "cd .. => $(basename $PWD)"
|
||||
)
|
||||
|
||||
- name: "OLDPWD"
|
||||
env:
|
||||
OLDPWD: "/nonexistent/path"
|
||||
stdin: |
|
||||
mkdir subdir
|
||||
|
||||
echo "[0]"
|
||||
echo ${OLDPWD+set}
|
||||
|
||||
echo "[1]"
|
||||
echo ${OLDPWD-NOTSET}
|
||||
|
||||
echo "[2]"
|
||||
declare -p OLDPWD 2>&1 | grep -o "declare -[a-z]* OLDPWD"
|
||||
|
||||
echo "[3]"
|
||||
env | grep "^OLDPWD=" || echo "NOT_IN_ENV"
|
||||
|
||||
echo
|
||||
echo "[Unsetting + cd'ing...]"
|
||||
unset OLDPWD
|
||||
cd $PWD/subdir
|
||||
|
||||
echo "[4]"
|
||||
declare -p OLDPWD 2>&1 | grep -o "declare -[a-z]* OLDPWD"
|
||||
|
||||
echo "[5]"
|
||||
env | grep -o "^OLDPWD="
|
||||
@@ -0,0 +1,13 @@
|
||||
name: "Builtins: colon"
|
||||
cases:
|
||||
- name: "Basic colon usage"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
:
|
||||
echo "Result 1: $?"
|
||||
: something
|
||||
echo "Result 2: $?"
|
||||
: --anything=here or here
|
||||
echo "Result 3: $?"
|
||||
: --help
|
||||
echo "Result 4: $?"
|
||||
@@ -0,0 +1,88 @@
|
||||
name: "Builtins: command"
|
||||
cases:
|
||||
- name: "Basic command usage"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "Executing echo built-in"
|
||||
command echo "Hello"
|
||||
|
||||
echo "Executing ls using name"
|
||||
command ls -d /
|
||||
|
||||
echo "Executing ls using absolute path"
|
||||
command $(which ls) -d /
|
||||
|
||||
echo "Executing non-existent command by name"
|
||||
command non-existent
|
||||
|
||||
echo "Executing non-existent command by path"
|
||||
command /usr/bin/non-existent
|
||||
|
||||
- name: "Non-executable command"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
mkdir subdir
|
||||
|
||||
echo "Before executing"
|
||||
./subdir
|
||||
echo "After executing: result=$?"
|
||||
|
||||
echo "Before and-or list execution"
|
||||
./subdir || true
|
||||
echo "After and-or list execution: result=$?"
|
||||
|
||||
- name: "command -v"
|
||||
stdin: |
|
||||
echo "PATH: $PATH"
|
||||
|
||||
echo "[echo]"
|
||||
command -v echo
|
||||
|
||||
echo "[non-existent]"
|
||||
command -v non-existent || echo "1. Not found"
|
||||
|
||||
echo "[/usr/bin/non-existent]"
|
||||
command -v /usr/bin/non-existent || echo "2. Not found"
|
||||
|
||||
- name: "command -v -p"
|
||||
stdin: |
|
||||
unset PATH
|
||||
|
||||
echo "[no -p]"
|
||||
command -v cat
|
||||
|
||||
echo "[-p]"
|
||||
command -v -p cat
|
||||
|
||||
- name: "command -v with full paths"
|
||||
stdin: |
|
||||
echo "[cat]"
|
||||
command -v cat
|
||||
|
||||
echo "[\$(command -v cat)]"
|
||||
command -v $(command -v cat)
|
||||
|
||||
- name: "command -V"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
command -V echo
|
||||
command -V ls
|
||||
command -V $(which ls)
|
||||
|
||||
command -V non-existent || echo "1. Not found"
|
||||
command -V /usr/bin/non-existent || echo "2. Not found"
|
||||
|
||||
- name: "command with --"
|
||||
stdin: |
|
||||
command -- ls
|
||||
|
||||
- name: "command with --help"
|
||||
stdin: |
|
||||
command ls --help
|
||||
|
||||
- name: "command with -p"
|
||||
ignore_stderr: true # In case it fails on the oracle as well, ignore the specific error message.
|
||||
stdin: |
|
||||
unset PATH
|
||||
|
||||
command -p -- ls
|
||||
@@ -0,0 +1,20 @@
|
||||
name: "Builtin Common Tests"
|
||||
cases:
|
||||
- name: "Piping builtin output"
|
||||
stdin: |
|
||||
shopt | wc -l | wc -l
|
||||
|
||||
- name: "Redirecting builtin output"
|
||||
stdin: |
|
||||
declare my_variable=10
|
||||
declare -p my_variable >out.txt
|
||||
|
||||
echo "Dumping file contents..."
|
||||
cat out.txt
|
||||
|
||||
- name: "Overrides"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
declare -p myvar
|
||||
myvar=10 declare -p myvar
|
||||
declare -p myvar
|
||||
@@ -0,0 +1,309 @@
|
||||
name: "Builtins: compgen"
|
||||
cases:
|
||||
- name: "compgen -A alias"
|
||||
stdin: |
|
||||
alias myalias="ls"
|
||||
alias myalias2="echo hi"
|
||||
|
||||
compgen -A alias myalias | sort
|
||||
|
||||
- name: "compgen -A binding"
|
||||
stdin: |
|
||||
compgen -A binding tab | sort
|
||||
|
||||
- name: "compgen -A builtin"
|
||||
stdin: |
|
||||
compgen -A builtin cd | sort
|
||||
|
||||
- name: "compgen -A directory"
|
||||
stdin: |
|
||||
touch somefile
|
||||
mkdir somedir
|
||||
mkdir somedir2
|
||||
|
||||
compgen -A directory some | sort
|
||||
|
||||
- name: "compgen -A file"
|
||||
stdin: |
|
||||
touch somefile
|
||||
mkdir somedir
|
||||
mkdir somedir2
|
||||
|
||||
compgen -A file some | sort
|
||||
|
||||
- name: "compgen -A file with dot files"
|
||||
stdin: |
|
||||
touch .file
|
||||
mkdir .dir
|
||||
|
||||
echo "[without dotglob]"
|
||||
shopt -u dotglob
|
||||
compgen -A file | sort
|
||||
|
||||
echo "[with dotglob]"
|
||||
shopt -s dotglob
|
||||
compgen -A file | sort
|
||||
|
||||
- name: "compgen -A function"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
echo hi
|
||||
}
|
||||
|
||||
myfunc2() {
|
||||
echo hello
|
||||
}
|
||||
|
||||
compgen -A function myfunc | sort
|
||||
|
||||
- name: "compgen -A keyword"
|
||||
stdin: |
|
||||
compgen -A keyword esa | sort
|
||||
|
||||
- name: "compgen -A variable"
|
||||
stdin: |
|
||||
declare myvar=10
|
||||
declare myvar2=11
|
||||
|
||||
compgen -A variable myvar | sort
|
||||
|
||||
- name: "compgen -W"
|
||||
stdin: |
|
||||
echo "1. Basic compgen -W"
|
||||
compgen -W "one two three" -- t | sort
|
||||
echo "2. compgen -W with expansion"
|
||||
myvar=value compgen -W '${myvar}'
|
||||
|
||||
- name: "compgen -W does not pathname-expand"
|
||||
stdin: |
|
||||
touch foo.txt bar.txt baz.log
|
||||
|
||||
echo "[glob pattern should be literal]"
|
||||
compgen -W '*.txt' | sort
|
||||
|
||||
echo "[question mark glob should be literal]"
|
||||
compgen -W '?.log' | sort
|
||||
|
||||
- name: "compgen -W with unsorted values"
|
||||
stdin: |
|
||||
compgen -W 'c b d a'
|
||||
|
||||
- name: "compgen -W with options"
|
||||
stdin: |
|
||||
compgen -W '--abc --def' -- '--ab'
|
||||
|
||||
- name: "compgen with no matches"
|
||||
stdin: |
|
||||
compgen -W yes no && echo "1. Result"
|
||||
|
||||
- name: "compgen -f with tilde"
|
||||
home_dir: testhome # relative to default working dir
|
||||
stdin: |
|
||||
mkdir testhome && cd testhome
|
||||
|
||||
echo "HOME base: $(basename $HOME)"
|
||||
echo "~ base:" $(basename ~)
|
||||
|
||||
touch item1
|
||||
|
||||
echo "[0]"
|
||||
for p in $(compgen -f ~); do
|
||||
echo ${p//$HOME/HOME}
|
||||
done
|
||||
|
||||
echo "[1]"
|
||||
for p in $(compgen -f ~/); do
|
||||
echo ${p//$HOME/HOME}
|
||||
done
|
||||
|
||||
- name: "compgen -f with quoted tilde"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
touch item1
|
||||
HOME=$(pwd)
|
||||
|
||||
echo "[0]"
|
||||
for p in $(compgen -f '~/'); do
|
||||
echo ${p//$HOME/HOME}
|
||||
done
|
||||
|
||||
- name: "compgen -f with quoted var"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
touch item1
|
||||
HOME=$(pwd)
|
||||
|
||||
echo "[0]"
|
||||
for p in $(compgen -f '$HOME/'); do
|
||||
echo ${p//$HOME/HOME}
|
||||
done
|
||||
|
||||
- name: "compgen -f with quoted + escaped var"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
touch item1
|
||||
HOME=$(pwd)
|
||||
|
||||
echo "[0]"
|
||||
for p in $(compgen -f '\$HOME/'); do
|
||||
echo ${p//$HOME/HOME}
|
||||
done
|
||||
|
||||
- name: "compgen with interesting hyphens"
|
||||
stdin: |
|
||||
compgen -P-before -S-after -W "one two three" -- t | sort
|
||||
|
||||
- name: "compgen -X"
|
||||
stdin: |
|
||||
echo "[Take 1]"
|
||||
compgen -W 'foo bar' -X 'foo' ''
|
||||
|
||||
echo "[Take 2]"
|
||||
compgen -W 'foo bar' -X 'f*' ''
|
||||
|
||||
echo "[Take 3]"
|
||||
compgen -W '&1 foo' -X '\&*' ''
|
||||
|
||||
- name: "compgen -X with replacement"
|
||||
stdin: |
|
||||
echo "[Take 1]"
|
||||
compgen -W 'somebody something' -X '&b*' some
|
||||
|
||||
- name: "compgen -X with extglob"
|
||||
stdin: |
|
||||
touch README
|
||||
shopt -s extglob
|
||||
|
||||
echo "[Take 1]"
|
||||
compgen -f READ
|
||||
|
||||
echo "[Take 2]"
|
||||
compgen -f -X "READ" READ
|
||||
|
||||
echo "[Take 3]"
|
||||
compgen -f -X "README" READ
|
||||
|
||||
echo "[Take 4]"
|
||||
compgen -f -X "!(READ)" READ
|
||||
|
||||
echo "[Take 5]"
|
||||
compgen -f -X "!(README)" READ
|
||||
|
||||
echo "[Take 6]"
|
||||
compgen -f -X "!!(READ)" READ
|
||||
|
||||
echo "[Take 7]"
|
||||
compgen -f -X "!!(README)" READ
|
||||
|
||||
- name: "compgen -o dirnames"
|
||||
stdin: |
|
||||
echo "[Take 1]"
|
||||
compgen -W 'completion' -o dirnames -- c | sort
|
||||
|
||||
echo "[Take 2]"
|
||||
mkdir subdir
|
||||
touch subfile
|
||||
compgen -W 'completion' -o dirnames -- s | sort
|
||||
|
||||
- name: "compgen -o default"
|
||||
stdin: |
|
||||
echo "[Take 1]"
|
||||
compgen -W 'completion' -o default -- c | sort
|
||||
|
||||
echo "[Take 2]"
|
||||
mkdir subdir
|
||||
touch subfile
|
||||
compgen -W 'completion' -o default -- s | sort
|
||||
|
||||
- name: "compgen -o bashdefault"
|
||||
stdin: |
|
||||
echo "[Take 1]"
|
||||
compgen -W 'completion' -o bashdefault -- c | sort
|
||||
|
||||
echo "[Take 2]"
|
||||
mkdir subdir
|
||||
touch subfile
|
||||
compgen -W 'completion' -o bashdefault -- s | sort
|
||||
|
||||
- name: "compgen -o plusdirs"
|
||||
stdin: |
|
||||
echo "[Take 1]"
|
||||
touch cfile
|
||||
mkdir cdir
|
||||
compgen -W 'completion' -o plusdirs -o default -S suffix -- c | sort
|
||||
|
||||
- name: "compgen -A command"
|
||||
# Temporarily marked unconstrained Path case as `skip: true` to keep CI stable.
|
||||
# Environment-dependent due to host PATH contents(Ubuntu includes X11, while Arch and others do not).
|
||||
skip: true
|
||||
stdin: |
|
||||
compgen -A command | sort | grep -v "^brush"
|
||||
|
||||
- name: "compgen -A command in a constrained PATH"
|
||||
stdin: |
|
||||
SORT=$(command -v sort)
|
||||
GREP=$(command -v grep)
|
||||
PATH=$(pwd)
|
||||
compgen -A command | $SORT | $GREP -v "^brush"
|
||||
|
||||
- name: "compgen -A command builtins in a constrained PATH"
|
||||
stdin: |
|
||||
SORT=$(command -v sort)
|
||||
PATH=$(pwd)
|
||||
compgen -A command cd | $SORT
|
||||
|
||||
- name: "compgen -A command keywords in a constrained PATH"
|
||||
stdin: |
|
||||
SORT=$(command -v sort)
|
||||
PATH=$(pwd)
|
||||
compgen -A command [ | $SORT
|
||||
|
||||
- name: "compgen -A command function in a constrained PATH"
|
||||
stdin: |
|
||||
SORT=$(command -v sort)
|
||||
PATH=$(pwd)
|
||||
myfunc() {
|
||||
echo hi
|
||||
}
|
||||
myfunc2() {
|
||||
echo hello
|
||||
}
|
||||
compgen -A command myfunc | $SORT
|
||||
|
||||
- name: "compgen -A command with executable symlink"
|
||||
stdin: |
|
||||
# pwd
|
||||
# drwxr-xr-x ├── test-bin
|
||||
# .rwxr-xr-x │ ├── dummy_exe
|
||||
# .rw-r--r-- │ └── dummy_non_exe
|
||||
# drwxr-xr-x └── test-links
|
||||
# lrwxrwxrwx ├── link_valid ⇒ $(pwd)/test-bin/dummy_exe
|
||||
# lrwxrwxrwx └── link_valid_exe_rec ⇒ $(pwd)/test-links/link_valid
|
||||
# lrwxrwxrwx ├── link_non_exec ⇒ $(pwd)/test-bin/dummy_non_exe
|
||||
# lrwxrwxrwx ├── link_broken ⇒ $(pwd)/test-bin/dummy_non_exist
|
||||
#
|
||||
mkdir test-bin
|
||||
touch test-bin/dummy_exe
|
||||
touch test-bin/dummy_non_exe
|
||||
chmod +x test-bin/dummy_exe
|
||||
|
||||
mkdir test-links
|
||||
ln -s $(pwd)/test-bin/dummy_exe $(pwd)/test-links/link_valid
|
||||
ln -s $(pwd)/test-links/link_vaild $(pwd)/test-links/link_valid_rec
|
||||
ln -s $(pwd)/test-bin/dummy_non_exe $(pwd)/test-links/link_non_exec
|
||||
ln -s $(pwd)/test-bin/dummy_non_exist $(pwd)/test-links/link_broken
|
||||
|
||||
PATH="$(pwd)/test-links:$PATH"
|
||||
|
||||
echo "[Valid]"
|
||||
compgen -A command -- link_valid | sort
|
||||
|
||||
echo "[Invalid]"
|
||||
compgen -A command -- link_non_exec
|
||||
compgen -A command -- link_broken
|
||||
|
||||
- name: "compgen -F sets COMP_KEY and COMP_TYPE to 0"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
_comp() { echo "KEY=${COMP_KEY:-unset} TYPE=${COMP_TYPE:-unset}"; COMPREPLY=(); }
|
||||
compgen -F _comp mycmd
|
||||
@@ -0,0 +1,114 @@
|
||||
name: "Builtins: complete"
|
||||
cases:
|
||||
- name: "complete with no options"
|
||||
stdin: |
|
||||
complete -W foo mycmd
|
||||
complete
|
||||
|
||||
- name: "Roundtrip: complete -W"
|
||||
stdin: |
|
||||
complete -W foo mycmd
|
||||
complete -p mycmd
|
||||
|
||||
complete -W 'foo bar' mycmd
|
||||
complete -p mycmd
|
||||
|
||||
- name: "Roundtrip: complete -P"
|
||||
stdin: |
|
||||
complete -P myprefix mycmd
|
||||
complete -p mycmd
|
||||
|
||||
complete -P 'my prefix' mycmd
|
||||
complete -p mycmd
|
||||
|
||||
- name: "Roundtrip: complete -S"
|
||||
stdin: |
|
||||
complete -S mysuffix mycmd
|
||||
complete -p mycmd
|
||||
|
||||
complete -S 'my suffix' mycmd
|
||||
complete -p mycmd
|
||||
|
||||
- name: "Roundtrip: complete -F"
|
||||
stdin: |
|
||||
complete -Fmyfunc mycmd
|
||||
complete -p mycmd
|
||||
|
||||
- name: "Roundtrip: complete -F"
|
||||
stdin: |
|
||||
complete -G pattern mycmd
|
||||
complete -p mycmd
|
||||
|
||||
complete -G 'pat tern' mycmd
|
||||
complete -p mycmd
|
||||
|
||||
- name: "Roundtrip: complete -X"
|
||||
stdin: |
|
||||
complete -X pattern mycmd
|
||||
complete -p mycmd
|
||||
|
||||
complete -X 'pat tern' mycmd
|
||||
complete -p mycmd
|
||||
|
||||
- name: "Roundtrip: complete -C"
|
||||
stdin: |
|
||||
complete -C cmd mycmd
|
||||
complete -p mycmd
|
||||
|
||||
complete -C 'c md' mycmd
|
||||
complete -p mycmd
|
||||
|
||||
- name: "Roundtrip: complete -A"
|
||||
stdin: |
|
||||
for action in alias arrayvar binding builtin command directory disabled enabled export file 'function' group helptopic hostname job keyword running service setopt shopt signal stopped user variable; do
|
||||
complete -A ${action} mycmd
|
||||
complete -p mycmd
|
||||
done
|
||||
|
||||
- name: "Roundtrip: complete -o options"
|
||||
stdin: |
|
||||
for opt in bashdefault default dirnames filenames noquote nosort nospace plusdirs; do
|
||||
echo "--- Testing option: ${opt} ------------------"
|
||||
complete -o ${opt} mycmd_${opt}
|
||||
complete -p mycmd_${opt}
|
||||
done
|
||||
|
||||
- name: "complete -r"
|
||||
stdin: |
|
||||
echo "[Removing non-existent]"
|
||||
complete -r nonexistent
|
||||
echo $?
|
||||
|
||||
echo "[Removing existing]"
|
||||
complete -W token mycmd
|
||||
complete -r mycmd
|
||||
echo $?
|
||||
complete -p mycmd 2>/dev/null
|
||||
|
||||
- name: "complete -r with no args"
|
||||
stdin: |
|
||||
complete -W token mycmd1
|
||||
complete -W token mycmd2
|
||||
|
||||
complete -r
|
||||
echo $?
|
||||
|
||||
complete -p
|
||||
|
||||
- name: "complete -r with special options"
|
||||
stdin: |
|
||||
complete -W token mycmd
|
||||
complete -W other -E
|
||||
|
||||
complete -r -E
|
||||
echo $?
|
||||
|
||||
complete -p
|
||||
|
||||
- name: "compgen -W preserves duplicates"
|
||||
stdin: |
|
||||
compgen -W "a b a c b"
|
||||
|
||||
- name: "compgen -W preserves order"
|
||||
stdin: |
|
||||
compgen -W "z y x a"
|
||||
@@ -0,0 +1,451 @@
|
||||
name: "Builtins: declare"
|
||||
common_test_files:
|
||||
- path: "helpers.sh"
|
||||
contents: |
|
||||
stable_print_assoc_array() {
|
||||
# TODO(nameref): enable use of nameref when implemented; for now
|
||||
# we assume the name of the array is assoc_array
|
||||
# local -n assoc_array=$1
|
||||
local key
|
||||
|
||||
for key in $(printf "%s\n" "${!assoc_array[@]}" | sort -n); do
|
||||
echo "\"${key}\" => ${assoc_array[${key}]}"
|
||||
done
|
||||
}
|
||||
|
||||
cases:
|
||||
- name: "Display vars"
|
||||
stdin: |
|
||||
declare myvar=something
|
||||
declare -p myvar
|
||||
|
||||
myarr=(a b c)
|
||||
declare -p myarr
|
||||
|
||||
- name: "Display vars with interesting chars"
|
||||
stdin: |
|
||||
(testvar="\"abc\"" && declare -p testvar && declare | grep testvar=)
|
||||
echo "-------------------------"
|
||||
(testvar="a b c" && declare -p testvar && declare | grep testvar=)
|
||||
echo "-------------------------"
|
||||
(testvar="'" && declare -p testvar && declare | grep testvar=)
|
||||
|
||||
- name: "Display vars with interesting chars 2"
|
||||
min_oracle_version: 5.2 # some sequences render differently in older shell versions
|
||||
stdin: |
|
||||
(testvar=$'a\nb' && declare -p testvar && declare | grep testvar=)
|
||||
echo "-------------------------"
|
||||
(testvar=$'\x03' && declare -p testvar && declare | grep testvar=)
|
||||
echo "-------------------------"
|
||||
(testvar=$'\x08' && declare -p testvar && declare | grep testvar=)
|
||||
echo "-------------------------"
|
||||
(testvar=$(printf '\033[34mabc\033[0m') && declare -p testvar && declare | grep testvar=)
|
||||
|
||||
- name: "Declare integer"
|
||||
stdin: |
|
||||
declare -i num=10
|
||||
declare -p num
|
||||
|
||||
echo $num
|
||||
num+=10
|
||||
echo $num
|
||||
|
||||
declare +i num
|
||||
declare -p num
|
||||
|
||||
echo $num
|
||||
num+=10
|
||||
echo $num
|
||||
|
||||
- name: "Declare integer with non-integer string"
|
||||
stdin: |
|
||||
declare -i var=value
|
||||
declare -p var
|
||||
|
||||
- name: "Update integer with non-integer string"
|
||||
stdin: |
|
||||
declare -i var=10
|
||||
declare -p var
|
||||
|
||||
var=value
|
||||
declare -p var
|
||||
|
||||
- name: "Update integer array with non-integer string"
|
||||
stdin: |
|
||||
declare -ai arr=()
|
||||
declare -p arr
|
||||
|
||||
arr[0]="value"
|
||||
declare -p arr
|
||||
|
||||
- name: "Update integer array with non-integer string"
|
||||
stdin: |
|
||||
declare -Ai arr=()
|
||||
declare -p arr
|
||||
|
||||
arr['key']="value"
|
||||
declare -p arr
|
||||
|
||||
- name: "Declare readonly variable"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
declare -r var="readonly"
|
||||
declare -p var
|
||||
|
||||
echo $var
|
||||
|
||||
var="change"
|
||||
echo "change result: $?"
|
||||
echo "var: ${var}"
|
||||
|
||||
declare +r var
|
||||
echo "+r result: $?"
|
||||
declare -p var
|
||||
|
||||
- name: "Declare array"
|
||||
stdin: |
|
||||
declare -a arr=("element1" "element2" "element3")
|
||||
declare -p arr
|
||||
echo "[0]: ${arr[0]}"
|
||||
echo "[1]: ${arr[0]}"
|
||||
echo "[2]: ${arr[0]}"
|
||||
echo "[3]: ${arr[0]}"
|
||||
echo "STAR: ${arr[*]}"
|
||||
echo "AT: ${arr[*]}"
|
||||
|
||||
- name: "Declare associative array"
|
||||
stdin: |
|
||||
declare -A arr=(["x1"]=1 ["x2"]=2)
|
||||
declare -p arr
|
||||
echo "[x]: ${arr[x1]}"
|
||||
echo "[y]: ${arr[x2]}"
|
||||
echo "[z]: ${arr[x3]}"
|
||||
echo "STAR: ${arr[*]}"
|
||||
echo "AT: ${arr[*]}"
|
||||
|
||||
- name: "Declare and export variable"
|
||||
stdin: |
|
||||
declare -x myexportedvar="exported variable"
|
||||
env | grep myexportedvar
|
||||
|
||||
- name: "Declare and export unset variable"
|
||||
stdin: |
|
||||
declare -x myunsetexportedvar
|
||||
env | grep myunsetexportedvar
|
||||
set | grep myunsetexportedvar=
|
||||
|
||||
- name: "Re-declaring variable"
|
||||
stdin: |
|
||||
var="value"
|
||||
declare var
|
||||
echo "var: ${var}"
|
||||
|
||||
- name: "Declaring without value"
|
||||
stdin: |
|
||||
[[ -v var ]] && echo "1: Variable is set"
|
||||
declare var
|
||||
declare -p var
|
||||
[[ -v var ]] && echo "2: Variable is set"
|
||||
declare var2=""
|
||||
declare -p var2
|
||||
[[ -v var2 ]] && echo "3: Variable is set"
|
||||
|
||||
- name: "Displaying local vars"
|
||||
stdin: |
|
||||
function test {
|
||||
echo "Dumping local variables (should be empty)"
|
||||
local -p
|
||||
|
||||
local -i int_var=10
|
||||
local -A assoc_array=(["x1"]=1 ["x2"]=2)
|
||||
local -a array=(a b c)
|
||||
local -r ro_var="readonly"
|
||||
local -t traced="value"
|
||||
|
||||
echo "Dump all variables"
|
||||
local -p
|
||||
}
|
||||
|
||||
test
|
||||
|
||||
- name: "Using local to detect function presence"
|
||||
stdin: |
|
||||
function test {
|
||||
local something 2>/dev/null && echo "In function"
|
||||
}
|
||||
|
||||
local something 2>/dev/null || echo "Not in function"
|
||||
|
||||
test
|
||||
|
||||
- name: "Displaying function names"
|
||||
stdin: |
|
||||
echo "Dumping function names"
|
||||
declare -F
|
||||
declare -p -F
|
||||
|
||||
function test {
|
||||
:
|
||||
}
|
||||
|
||||
echo "Dumping function names again"
|
||||
declare -F
|
||||
declare -p -F
|
||||
|
||||
echo "Dumping test"
|
||||
declare -F test
|
||||
declare -p -F test
|
||||
|
||||
- name: "Displaying functions"
|
||||
stdin: |
|
||||
echo "Dumping functions"
|
||||
declare -f
|
||||
declare -p -f
|
||||
|
||||
function test {
|
||||
:
|
||||
}
|
||||
|
||||
echo "Dumping functions again"
|
||||
declare -f
|
||||
declare -p -f
|
||||
|
||||
echo "Dumping test"
|
||||
declare -f test
|
||||
declare -p -f test
|
||||
|
||||
- name: "Displaying non-existent functions"
|
||||
stdin: |
|
||||
declare -f not_a_function
|
||||
echo "Result (-f): $?"
|
||||
|
||||
declare -F not_a_function
|
||||
echo "Result (-F): $?"
|
||||
|
||||
- name: "Valid conversions"
|
||||
stdin: |
|
||||
declare -a arr1=(a b c)
|
||||
declare -a arr1
|
||||
echo "Conversion result: $?"
|
||||
declare -p arr1
|
||||
|
||||
declare -A arr2=(["x1"]=1 ["x2"]=2)
|
||||
declare -A arr2
|
||||
echo "Conversion result: $?"
|
||||
declare -p arr2
|
||||
|
||||
declare scalar1="value"
|
||||
declare -a scalar1
|
||||
echo "Conversion result: $?"
|
||||
declare -p scalar1
|
||||
|
||||
declare scalar2="value"
|
||||
declare -A scalar2
|
||||
echo "Conversion result: $?"
|
||||
declare -p scalar2
|
||||
|
||||
- name: "Bad conversions"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
declare -a arr1=(a b c)
|
||||
declare -A arr1
|
||||
echo "Conversion result: $?"
|
||||
declare -p arr1
|
||||
|
||||
declare -A arr2=(["x1"]=1 ["x2"]=2)
|
||||
declare -a arr2
|
||||
echo "Conversion result: $?"
|
||||
declare -p arr2
|
||||
|
||||
- name: "Declare -p using invalid forms"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
declare arr=(a b c)
|
||||
declare -p arr[0]
|
||||
echo "Result: $?"
|
||||
declare -p arr[0]=1
|
||||
echo "Result: $?"
|
||||
|
||||
declare scalar=x
|
||||
echo "Result: $?"
|
||||
declare -p scalar=y
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Updating value"
|
||||
stdin: |
|
||||
declare var="value"
|
||||
declare -p var
|
||||
declare var="changed"
|
||||
declare -p var
|
||||
|
||||
- name: "Updating value attributes"
|
||||
stdin: |
|
||||
declare -ix var=10
|
||||
declare -p var
|
||||
declare +ix var
|
||||
declare -p var
|
||||
|
||||
- name: "Updating array"
|
||||
stdin: |
|
||||
declare arr=(a b c)
|
||||
declare -p arr
|
||||
declare arr=(d e)
|
||||
declare -p arr
|
||||
declare arr=10
|
||||
declare -p arr
|
||||
|
||||
declare arr2=a
|
||||
declare -p arr2
|
||||
declare -A arr2=(["key"]="value")
|
||||
declare -p arr2
|
||||
|
||||
- name: "Updating causing conversion"
|
||||
stdin: |
|
||||
source helpers.sh
|
||||
|
||||
declare assoc_array="scalar-value"
|
||||
declare -p assoc_array
|
||||
|
||||
declare -A assoc_array["key"]="key-value"
|
||||
stable_print_assoc_array assoc_array
|
||||
|
||||
- name: "Uppercase attribute"
|
||||
stdin: |
|
||||
declare var=value
|
||||
|
||||
declare -u var
|
||||
declare -p var
|
||||
|
||||
var="abcd"
|
||||
declare -p var
|
||||
|
||||
declare another="abcd"
|
||||
declare -u another
|
||||
another+=efg
|
||||
declare -p another
|
||||
|
||||
- name: "Lowercase attribute"
|
||||
stdin: |
|
||||
declare var=value
|
||||
|
||||
declare -l var
|
||||
declare -p var
|
||||
|
||||
var="AbCd"
|
||||
declare -p var
|
||||
|
||||
declare another="ABCD"
|
||||
declare -l another
|
||||
another+=EFG
|
||||
declare -p another
|
||||
|
||||
- name: "Capitalize attribute"
|
||||
stdin: |
|
||||
declare var=value
|
||||
|
||||
declare -c var
|
||||
declare -p var
|
||||
|
||||
var="aBcD eFg"
|
||||
declare -p var
|
||||
|
||||
declare another="aBcD"
|
||||
declare -c another
|
||||
another+=eFg
|
||||
declare -p another
|
||||
|
||||
- name: "Declare invalid identifiers"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
declare 1=x && echo "Set 1"
|
||||
declare @=10 && echo "Set @"
|
||||
|
||||
- name: "Nameref declare -p shows nameref attribute"
|
||||
stdin: |
|
||||
target="hello"
|
||||
declare -n ref=target
|
||||
declare -p ref
|
||||
declare -p target
|
||||
|
||||
- name: "Nameref declare -n with initial value"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
target="original"
|
||||
declare -n ref=target
|
||||
echo "ref: $ref"
|
||||
echo "target: $target"
|
||||
|
||||
- name: "Nameref remove attribute with +n"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
target="value"
|
||||
declare -n ref=target
|
||||
echo "as nameref: $ref"
|
||||
declare +n ref
|
||||
echo "after +n: ref=$ref"
|
||||
declare -p ref
|
||||
|
||||
- name: "Nameref to array via declare"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
arr=(one two three)
|
||||
declare -n ref=arr
|
||||
echo "ref[0]: ${ref[0]}"
|
||||
echo "ref[1]: ${ref[1]}"
|
||||
echo "ref[@]: ${ref[@]}"
|
||||
echo "ref[*]: ${ref[*]}"
|
||||
|
||||
- name: "Nameref array modification via declare"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
arr=(a b c)
|
||||
declare -n ref=arr
|
||||
ref[1]="modified"
|
||||
echo "arr[1]: ${arr[1]}"
|
||||
echo "ref[1]: ${ref[1]}"
|
||||
|
||||
- name: "Nameref to associative array via declare"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
declare -A assoc=(["key1"]="val1" ["key2"]="val2")
|
||||
declare -n ref=assoc
|
||||
echo "ref[key1]: ${ref[key1]}"
|
||||
echo "ref[key2]: ${ref[key2]}"
|
||||
|
||||
- name: "Nameref array iteration in function via local -n"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
print_array() {
|
||||
local -n arr=$1
|
||||
echo "count: ${#arr[@]}"
|
||||
for elem in "${arr[@]}"; do
|
||||
echo "elem: $elem"
|
||||
done
|
||||
}
|
||||
|
||||
myarr=(first second third)
|
||||
print_array myarr
|
||||
|
||||
- name: "Nameref get keys via declare"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
declare -A assoc=(["a"]=1 ["b"]=2 ["c"]=3)
|
||||
declare -n ref=assoc
|
||||
echo "keys (sorted): $(printf '%s\n' ${!ref[@]} | sort | tr '\n' ' ')"
|
||||
echo "count: ${#ref[@]}"
|
||||
|
||||
- name: "Nameref array length via declare"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
arr=(one two three four five)
|
||||
declare -n ref=arr
|
||||
echo "length: ${#ref[@]}"
|
||||
echo "first element length: ${#ref[0]}"
|
||||
|
||||
- name: "Nameref declare -n filter"
|
||||
stdin: |
|
||||
regular="plain"
|
||||
declare -n ref1=regular
|
||||
declare -n ref2=regular
|
||||
not_a_ref="also plain"
|
||||
declare -p -n 2>/dev/null | sort
|
||||
@@ -0,0 +1,135 @@
|
||||
name: "Builtins: dot"
|
||||
cases:
|
||||
- name: "Basic dot usage"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "In sourced script"
|
||||
var="updated"
|
||||
stdin: |
|
||||
var="orig"
|
||||
. script.sh
|
||||
echo "var: ${var}"
|
||||
|
||||
- name: "Basic source usage"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "In sourced script"
|
||||
for f in ${FUNCNAME[@]}; do
|
||||
echo "FUNCNAME: '${f}'"
|
||||
done
|
||||
for s in ${BASH_SOURCE[@]}; do
|
||||
echo "BASH_SOURCE: '${s}'"
|
||||
done
|
||||
var="updated"
|
||||
|
||||
function script_func() {
|
||||
echo "In script func"
|
||||
for f in ${FUNCNAME[@]}; do
|
||||
echo "FUNCNAME: '${f}'"
|
||||
done
|
||||
for s in ${BASH_SOURCE[@]}; do
|
||||
echo "BASH_SOURCE: '${s}'"
|
||||
done
|
||||
}
|
||||
stdin: |
|
||||
var="orig"
|
||||
source ./script.sh
|
||||
echo "var: ${var}"
|
||||
script_func
|
||||
|
||||
- name: "Source non-existent file path"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
source script.sh
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Source dir"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
source .
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Source script without args"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "In sourced script"
|
||||
echo "Args: $@"
|
||||
stdin: |
|
||||
set -- outer1 outer2 outer3
|
||||
source script.sh
|
||||
|
||||
- name: "Source script with args"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "In sourced script"
|
||||
echo "Args: $@"
|
||||
stdin: |
|
||||
set -- outer1 outer2 outer3
|
||||
source script.sh inner1 inner2
|
||||
|
||||
- name: "Source with redirection"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "In sourced script"
|
||||
stdin: |
|
||||
source script.sh arg1 arg2 > out.txt
|
||||
echo "Sourced script; dumping..."
|
||||
cat out.txt
|
||||
|
||||
- name: "Source with return"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "Before return"
|
||||
return 42
|
||||
echo "After return"
|
||||
stdin: |
|
||||
echo "Before source"
|
||||
source script.sh
|
||||
echo "After source, exit code: $?"
|
||||
|
||||
- name: "Source with exit"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "Before exit"
|
||||
exit 42
|
||||
echo "After exit"
|
||||
stdin: |
|
||||
echo "Before source"
|
||||
source script.sh
|
||||
echo "After source"
|
||||
|
||||
- name: "Source with break in caller's loop"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "In script, breaking"
|
||||
break
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Iteration $i"
|
||||
source script.sh
|
||||
echo "After source"
|
||||
done
|
||||
echo "After loop"
|
||||
|
||||
- name: "Source with continue in caller's loop"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "In script, continuing"
|
||||
continue
|
||||
echo "After continue"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Iteration $i"
|
||||
source script.sh
|
||||
echo "After source"
|
||||
done
|
||||
echo "After loop"
|
||||
@@ -0,0 +1,12 @@
|
||||
name: "Builtins: echo"
|
||||
cases:
|
||||
- name: "echo with only --"
|
||||
stdin: echo --
|
||||
|
||||
- name: "echo with -- and args"
|
||||
stdin: echo -- -1 --"aaa" ?^1as-
|
||||
|
||||
- name: "echo -e"
|
||||
stdin: |
|
||||
echo -e "1. \x65" | hexdump -C
|
||||
echo -e "2. \x{65}" | hexdump -C
|
||||
@@ -0,0 +1,32 @@
|
||||
name: "Builtins: enable"
|
||||
cases:
|
||||
- name: "List special builtins"
|
||||
stdin: enable -s
|
||||
|
||||
- name: "List default-disabled builtins"
|
||||
stdin: enable -n
|
||||
|
||||
- name: "List all builtins"
|
||||
stdin: |
|
||||
# List builtins but ignore any brush specific ones.
|
||||
enable | grep -v brush
|
||||
|
||||
- name: "Disable builtins"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
type printf
|
||||
|
||||
# Disable the builtin
|
||||
PATH=
|
||||
enable -n printf
|
||||
|
||||
# Check
|
||||
type printf
|
||||
print "Gone\n"
|
||||
|
||||
# Re-enable
|
||||
enable printf
|
||||
|
||||
# Re-check
|
||||
type printf
|
||||
printf "Back\n"
|
||||
@@ -0,0 +1,47 @@
|
||||
name: "Builtins: eval"
|
||||
cases:
|
||||
- name: "Basic eval usage"
|
||||
stdin: |
|
||||
eval 'echo 1 + 1 == $((1 + 1))'
|
||||
|
||||
- name: "eval with return in function"
|
||||
stdin: |
|
||||
f() {
|
||||
eval 'return 42'
|
||||
echo "Should not reach here"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "eval with exit"
|
||||
stdin: |
|
||||
eval 'exit 42'
|
||||
echo "After eval'd exit"
|
||||
|
||||
- name: "eval with break in loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
eval 'break'
|
||||
echo "After eval'd break"
|
||||
done
|
||||
echo "After loop"
|
||||
|
||||
- name: "eval with continue in loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Iteration $i"
|
||||
eval 'continue'
|
||||
echo "After eval'd continue"
|
||||
done
|
||||
echo "After loop"
|
||||
|
||||
- name: "eval with nested break levels"
|
||||
stdin: |
|
||||
for i in 1 2; do
|
||||
for j in a b c; do
|
||||
eval 'break 2'
|
||||
echo "After eval'd break (inner loop)"
|
||||
done
|
||||
echo "After eval'd break (outer loop)"
|
||||
done
|
||||
echo "After outer loop"
|
||||
@@ -0,0 +1,51 @@
|
||||
name: "Builtins: exec"
|
||||
cases:
|
||||
- name: "Exec an invalid path"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
exec /some/nonexistent/path
|
||||
|
||||
- name: "Exec with no arguments"
|
||||
stdin: |
|
||||
pid=$$
|
||||
exec
|
||||
[[ "${pid}" == "$$" ]] || echo "PID changed"
|
||||
|
||||
- name: "Exec with no arguments and redirection for new fd"
|
||||
stdin: |
|
||||
exec 3>some-file.txt
|
||||
echo "Hello, fd 3!" >&3
|
||||
|
||||
- name: "Exec with no arguments and redirection of stderr"
|
||||
stdin: |
|
||||
exec 2>/dev/null
|
||||
|
||||
# Do something that should display an error to stderr
|
||||
source /non-existent
|
||||
|
||||
- name: "Exec with no arguments and redirection in a script"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
exec 2>output-file.txt
|
||||
echo hi >&2
|
||||
args: ["./script.sh"]
|
||||
|
||||
- name: "Exec a command"
|
||||
stdin: |
|
||||
exec $0 -c 'echo "In child shell"'
|
||||
echo "This is never reached"
|
||||
|
||||
- name: "exec without -c"
|
||||
stdin: |
|
||||
export myvar=value
|
||||
exec $0 -c 'echo "myvar: ${myvar}"'
|
||||
|
||||
- name: "exec -c"
|
||||
stdin: |
|
||||
export myvar=value
|
||||
exec -c $0 -c 'echo "myvar: ${myvar}"'
|
||||
|
||||
- name: "exec -a"
|
||||
stdin: |
|
||||
exec -a shellname $0 -c 'echo "0: $0"'
|
||||
@@ -0,0 +1,85 @@
|
||||
name: "Builtins: exit"
|
||||
cases:
|
||||
- name: "Exit without code"
|
||||
stdin: |
|
||||
exit
|
||||
|
||||
- name: "Exit with code"
|
||||
stdin: |
|
||||
exit 10
|
||||
|
||||
- name: "Exit with i64 max"
|
||||
stdin: |
|
||||
exit 9223372036854775807
|
||||
|
||||
- name: "Exit with i64 min"
|
||||
stdin: |
|
||||
exit -9223372036854775808
|
||||
|
||||
- name: "Exit in for loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
exit 42
|
||||
echo "Got past exit in loop"
|
||||
done
|
||||
echo "Got past loop"
|
||||
|
||||
- name: "Exit in arithmetic for loop body"
|
||||
stdin: |
|
||||
for ((i = 0; i < 10; i++)); do
|
||||
exit 42
|
||||
echo "Got past exit in loop"
|
||||
done
|
||||
echo "Got past loop"
|
||||
|
||||
- name: "Exit in while loop condition"
|
||||
stdin: |
|
||||
while exit 42; do
|
||||
echo "In loop"
|
||||
done
|
||||
echo "Got past loop"
|
||||
|
||||
- name: "Exit in while loop body"
|
||||
stdin: |
|
||||
while true; do
|
||||
exit 42
|
||||
echo "Got past exit in body"
|
||||
break
|
||||
done
|
||||
echo "Got past loop"
|
||||
|
||||
- name: "Exit in sequence"
|
||||
stdin: |
|
||||
exit 42; echo "Should not be printed"
|
||||
|
||||
- name: "Exit in function"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
exit 42
|
||||
echo "Got past exit in function"
|
||||
}
|
||||
|
||||
myfunc
|
||||
echo "Got past function call"
|
||||
|
||||
- name: "Exit in subshell"
|
||||
stdin: |
|
||||
(exit 42)
|
||||
echo "Got past subshell"
|
||||
|
||||
- name: "Exit in command substitution"
|
||||
stdin: |
|
||||
output=$(echo hi; exit 42; echo there)
|
||||
echo "Got past command substitution"
|
||||
|
||||
- name: "Exit in and/or"
|
||||
stdin: |
|
||||
exit 42 || echo "Got past exit"
|
||||
|
||||
- name: "Exit in brace group"
|
||||
stdin: |
|
||||
{
|
||||
exit 42
|
||||
echo "Got past exit"
|
||||
}
|
||||
echo "Got past brace group"
|
||||
@@ -0,0 +1,36 @@
|
||||
name: "Builtins: export"
|
||||
cases:
|
||||
- name: "Basic export usage"
|
||||
stdin: |
|
||||
MY_TEST_VAR="value"
|
||||
echo "Looking for unexported variable..."
|
||||
env | grep MY_TEST_VAR
|
||||
|
||||
echo "Exporting and looking for it again..."
|
||||
export MY_TEST_VAR
|
||||
env | grep MY_TEST_VAR
|
||||
|
||||
echo "Exporting with new value..."
|
||||
export MY_TEST_VAR="changed value"
|
||||
env | grep MY_TEST_VAR
|
||||
|
||||
- name: "Exporting array"
|
||||
stdin: |
|
||||
export arr=(a 1 2)
|
||||
declare -p arr
|
||||
|
||||
- name: "Unexporting variable"
|
||||
stdin: |
|
||||
MY_TEST_VAR="value"
|
||||
export MY_TEST_VAR="value"
|
||||
export -n MY_TEST_VAR
|
||||
|
||||
env | grep MY_TEST_VAR
|
||||
|
||||
- name: "Export with assignment through nameref"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
declare -n ref=MY_EXPORT_VAR
|
||||
export ref="exported_value"
|
||||
echo "MY_EXPORT_VAR: $MY_EXPORT_VAR"
|
||||
env | grep MY_EXPORT_VAR
|
||||
@@ -0,0 +1,275 @@
|
||||
name: "Builtins: fc"
|
||||
cases:
|
||||
- name: "fc -l lists recent commands"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
fc -l
|
||||
|
||||
- name: "fc -l with range (positive indices)"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
: cmd4
|
||||
: cmd5
|
||||
fc -l 2 4
|
||||
|
||||
- name: "fc -l with single argument"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
fc -l 2
|
||||
|
||||
- name: "fc -l with negative indices"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
: cmd4
|
||||
: cmd5
|
||||
fc -l -3 -1
|
||||
|
||||
- name: "fc -l with string search"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
: cmd1
|
||||
: other
|
||||
: cmd2
|
||||
: something
|
||||
fc -l cmd
|
||||
|
||||
- name: "fc -l with -n (no line numbers)"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
fc -ln
|
||||
|
||||
- name: "fc -l with -r (reverse order)"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
fc -lr
|
||||
|
||||
- name: "fc -l with -n and -r combined"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
fc -lnr
|
||||
|
||||
- name: "fc -l with range and -r"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
: cmd4
|
||||
: cmd5
|
||||
fc -lr 2 4
|
||||
|
||||
- name: "fc -l 0 means -1"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
fc -l 0
|
||||
|
||||
- name: "fc -l defaults to last 16 commands"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
: cmd4
|
||||
: cmd5
|
||||
: cmd6
|
||||
: cmd7
|
||||
: cmd8
|
||||
: cmd9
|
||||
: cmd10
|
||||
: cmd11
|
||||
: cmd12
|
||||
: cmd13
|
||||
: cmd14
|
||||
: cmd15
|
||||
: cmd16
|
||||
: cmd17
|
||||
: cmd18
|
||||
fc -l
|
||||
|
||||
- name: "fc -s re-executes last command"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo hello
|
||||
fc -s
|
||||
|
||||
- name: "fc -s with command argument"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo hello
|
||||
echo world
|
||||
fc -s echo
|
||||
|
||||
- name: "fc -s with pattern replacement"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo hello
|
||||
fc -s hello=goodbye
|
||||
|
||||
- name: "fc -s with command number"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo first
|
||||
echo second
|
||||
echo third
|
||||
fc -s 1
|
||||
|
||||
- name: "fc -s with negative offset"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo first
|
||||
echo second
|
||||
echo third
|
||||
fc -s -2
|
||||
|
||||
- name: "fc -s with string prefix match"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo aaa
|
||||
echo bbb
|
||||
echo aac
|
||||
fc -s aa
|
||||
|
||||
- name: "fc -s with pattern replacement and command number"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo first
|
||||
echo second
|
||||
echo hello
|
||||
fc -s hello=goodbye 3
|
||||
|
||||
- name: "fc -s with pattern replacement and string prefix"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo aaa
|
||||
: somecommand
|
||||
echo hello world
|
||||
fc -s hello=goodbye echo
|
||||
|
||||
- name: "fc -s with pattern replacement and negative index"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo one
|
||||
echo two
|
||||
echo hello world
|
||||
fc -s hello=goodbye -1
|
||||
|
||||
- name: "fc -s pattern replacement when pattern doesn't match"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo hello
|
||||
fc -s xyz=abc
|
||||
|
||||
- name: "fc -s pattern replacement replaces all occurrences"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo test test test
|
||||
fc -s test=foo
|
||||
|
||||
- name: "fc -s with empty replacement"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo hello world
|
||||
fc -s hello=
|
||||
|
||||
- name: "fc with invalid range"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
fc -l 100 200
|
||||
expect_status: 1
|
||||
|
||||
- name: "fc -s with nonexistent command"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo hello
|
||||
fc -s xyz
|
||||
expect_status: 1
|
||||
|
||||
- name: "fc -l with both string and number"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd3
|
||||
: other
|
||||
fc -l cmd 3
|
||||
@@ -0,0 +1,476 @@
|
||||
name: "Builtins: getopts"
|
||||
cases:
|
||||
- name: "Basic getopts"
|
||||
stdin: |
|
||||
func() {
|
||||
echo "Beginning of args"
|
||||
while getopts "ab:" option "$@"; do
|
||||
case ${option} in
|
||||
a) echo "Option a; OPTARG=${OPTARG}";;
|
||||
b) echo "Option b; OPTARG=${OPTARG}";;
|
||||
*) echo "Unknown option: ${option}";;
|
||||
esac
|
||||
echo "OPTIND is now ${OPTIND}"
|
||||
echo "OPTERR is now ${OPTERR}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Result: $?"
|
||||
echo "option: ${option}"
|
||||
echo "End of args"
|
||||
}
|
||||
|
||||
func -a -b my_b_arg
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: no args"
|
||||
stdin: |
|
||||
getopts "a:b:" myvar
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
|
||||
- name: "getopts: toplevel positional parameters"
|
||||
stdin: |
|
||||
set -- -a value
|
||||
getopts "a:b:" myvar
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
|
||||
- name: "getopts: function positional parameters"
|
||||
stdin: |
|
||||
my_function() {
|
||||
getopts "a:b:" myvar
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
}
|
||||
|
||||
my_function -b value
|
||||
|
||||
- name: "getopts: function script parameters"
|
||||
test_files:
|
||||
- path: script.sh
|
||||
contents: |
|
||||
getopts "a:b:" myvar
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
stdin: |
|
||||
source ./script.sh -a value
|
||||
|
||||
- name: "getopts: no options given"
|
||||
stdin: |
|
||||
while getopts "a:b:" myvar notoption; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Done; result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
|
||||
- name: "getopts: options and non-options"
|
||||
stdin: |
|
||||
while getopts "ab:" myvar -a -b value notoption; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Done; result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
|
||||
- name: "getopts: --"
|
||||
stdin: |
|
||||
while getopts "ab:" myvar -a -b value -- -c notoption; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Done; result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
|
||||
- name: "getopts: -- as first arg"
|
||||
stdin: |
|
||||
while getopts "ab:" myvar -- -a; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Done; result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
|
||||
- name: "getopts: invalid option when optstr starts with colon"
|
||||
stdin: |
|
||||
getopts ":a:" myvar -b value
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
|
||||
- name: "getopts: invalid option when optstr does not start with colon"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
getopts "a:" myvar -b value
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
|
||||
- name: "getopts: multiple options in one token"
|
||||
stdin: |
|
||||
while getopts "a:bcdefg" myvar -fedcba something -g; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Done; result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
|
||||
- name: "getopts: short option and value in single arg"
|
||||
stdin: |
|
||||
getopts "p:" myvar -p-
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
|
||||
while getopts "abep:" myvar -abpsomething-else; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "OPTERR: ${OPTERR}"
|
||||
echo "-----------------"
|
||||
done
|
||||
|
||||
- name: "getopts: OPTERR=0 suppresses error for unknown option"
|
||||
stdin: |
|
||||
OPTERR=0
|
||||
getopts "a:" myvar -x 2>/dev/null
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: OPTERR=1 shows error for unknown option"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
OPTERR=1
|
||||
getopts "a:" myvar -x
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: OPTERR nonzero shows error"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
OPTERR=42
|
||||
getopts "a:" myvar -x
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
|
||||
- name: "getopts: missing required argument without leading colon"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
getopts "a:" myvar -a
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: missing required argument with leading colon"
|
||||
stdin: |
|
||||
getopts ":a:" myvar -a
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: missing required argument with OPTERR=0"
|
||||
stdin: |
|
||||
OPTERR=0
|
||||
getopts "a:" myvar -a 2>/dev/null
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: combined unknown and missing arg errors"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
func() {
|
||||
while getopts "a:b:" opt "$@"; do
|
||||
case "$opt" in
|
||||
a) echo "a=$OPTARG" ;;
|
||||
b) echo "b=$OPTARG" ;;
|
||||
\?) echo "unknown" ;;
|
||||
:) echo "missing arg for $OPTARG" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
func -a foo -x -b
|
||||
echo "Done: $?"
|
||||
|
||||
- name: "getopts: bare dash as argument"
|
||||
stdin: |
|
||||
getopts "ab:" myvar -
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: leading colon with dash-colon option"
|
||||
stdin: |
|
||||
getopts ":a:" myvar -:
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: unknown option in middle of combined flags"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
while getopts "ac" myvar -axc; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Done; result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
|
||||
- name: "getopts: missing arg in combined flags"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
while getopts "a:b" myvar -ba; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Final: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: combined flags with arg-taking option consumes next arg"
|
||||
stdin: |
|
||||
while getopts "a:b" myvar -ba nextarg; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Done; result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: OPTIND reset between calls"
|
||||
stdin: |
|
||||
while getopts "a:b" myvar -b -a foo; do
|
||||
echo "myvar: ${myvar}; OPTARG: ${OPTARG-UNSET}; OPTIND: ${OPTIND}"
|
||||
done
|
||||
echo "--- reset ---"
|
||||
OPTIND=1
|
||||
while getopts "a:b" myvar -b -a bar; do
|
||||
echo "myvar: ${myvar}; OPTARG: ${OPTARG-UNSET}; OPTIND: ${OPTIND}"
|
||||
done
|
||||
|
||||
- name: "getopts: empty optstring"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
getopts "" myvar -a
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: OPTIND=0"
|
||||
stdin: |
|
||||
OPTIND=0
|
||||
getopts "a:" myvar -a value
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: negative OPTIND treated as reset"
|
||||
stdin: |
|
||||
OPTIND=-1
|
||||
getopts "a:" myvar -a value
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: bare dash among other args"
|
||||
stdin: |
|
||||
while getopts "ab" myvar -a - -b; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Done; result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: OPTIND past end of args"
|
||||
stdin: |
|
||||
OPTIND=100
|
||||
getopts "a:b:" myvar -a value
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: missing arg with leading colon in combined flags"
|
||||
stdin: |
|
||||
while getopts ":a:b" myvar -ba; do
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
echo "----------------"
|
||||
done
|
||||
echo "Done; result: $?"
|
||||
|
||||
- name: "getopts: non-numeric OPTIND treated as reset"
|
||||
stdin: |
|
||||
OPTIND=abc
|
||||
getopts "a:" myvar -a value
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: option value is empty string"
|
||||
stdin: |
|
||||
getopts "a:" myvar -a ""
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: '${OPTARG}'"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: option value looks like another option"
|
||||
stdin: |
|
||||
getopts "a:b" myvar -a -b
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: OPTIND=0 resets mid-parse of combined options"
|
||||
stdin: |
|
||||
getopts "abc" myvar -abc
|
||||
echo "first: ${myvar}"
|
||||
OPTIND=0
|
||||
getopts "abc" myvar -abc
|
||||
echo "after reset: ${myvar}"
|
||||
|
||||
- name: "getopts: OPTERR non-numeric treated as enabled"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
OPTERR=abc
|
||||
getopts "a:" myvar -x
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: invalid variable name rejected"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
getopts "a" "123abc" -a
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "getopts: duplicate option char in optstring uses first definition"
|
||||
stdin: |
|
||||
getopts "aa:" myvar -a val
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: optstring is only a colon"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
getopts ":" myvar -a
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTARG: ${OPTARG-UNSET}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts: empty string argument is not an option"
|
||||
stdin: |
|
||||
getopts "a" myvar ""
|
||||
echo "Result: $?"
|
||||
echo "myvar: ${myvar}"
|
||||
echo "OPTIND: ${OPTIND}"
|
||||
|
||||
- name: "getopts writes through nameref"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
declare -n ref=result
|
||||
getopts "a:b" ref -a value
|
||||
echo "Result: $?"
|
||||
echo "result: ${result}"
|
||||
echo "ref: ${ref}"
|
||||
echo "OPTARG: ${OPTARG}"
|
||||
|
||||
- name: "getopts writes through subscripted nameref"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
arr=(x x x)
|
||||
declare -n ref='arr[1]'
|
||||
getopts "a:b" ref -a value
|
||||
echo "Result: $?"
|
||||
echo "arr[1]: ${arr[1]}"
|
||||
echo "ref: ${ref}"
|
||||
@@ -0,0 +1,69 @@
|
||||
name: "Builtins: hash"
|
||||
cases:
|
||||
- name: "Non-existent program"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
hash non-existent-program
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Re-hash non-existent program"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
hash -p /some/path non-existent-program
|
||||
hash -t non-existent-program && echo "1. Result: $?"
|
||||
hash non-existent-program && echo "2. Result: $?"
|
||||
hash -t non-existent-program && echo "3. Result: $?"
|
||||
|
||||
- name: "Existent program"
|
||||
stdin: |
|
||||
hash ls
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Display not-yet-hashed program"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
hash -t ls
|
||||
echo "1. Result: $?"
|
||||
ls >/dev/null 2>&1
|
||||
hash -t ls
|
||||
echo "2. Result: $?"
|
||||
|
||||
- name: "Display hashed program"
|
||||
stdin: |
|
||||
hash -p /some/path somecmd && echo "1. Result: $?"
|
||||
hash -t somecmd && echo "2. Result: $?"
|
||||
|
||||
- name: "Display multiple hashed programs"
|
||||
stdin: |
|
||||
hash -p /some/path somecmd1 && echo "1. Result: $?"
|
||||
hash -p /some/path somecmd2 && echo "2. Result: $?"
|
||||
hash -t somecmd1 somecmd2 && echo "3. Result: $?"
|
||||
|
||||
- name: "Display -l path"
|
||||
stdin: |
|
||||
hash -p "/some/spaces path" somecmd && echo "1. Result: $?"
|
||||
hash -t somecmd && echo "2. Result: $?"
|
||||
hash -t -l somecmd && echo "3. Result: $?"
|
||||
|
||||
- name: "Remove"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
hash -p /some/path somecmd && echo "1. Result: $?"
|
||||
hash -t somecmd && echo "2. Result: $?"
|
||||
hash -d somecmd && echo "3. Result: $?"
|
||||
hash -t somecmd && echo "4. Result: $?"
|
||||
|
||||
- name: "Remove all"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
hash -p /some/path somecmd1 && echo "1. Result: $?"
|
||||
hash -p /some/path somecmd2 && echo "2. Result: $?"
|
||||
hash -r && echo "3. Result: $?"
|
||||
hash -t somecmd1 && echo "4. Result: $?"
|
||||
hash -t somecmd2 && echo "5. Result: $?"
|
||||
|
||||
- name: "Hash file path"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
hash /dev/null && echo "1. Result: $?"
|
||||
hash /dev/nulll && echo "2. Result: $?"
|
||||
@@ -0,0 +1,13 @@
|
||||
name: "Builtins: help"
|
||||
cases:
|
||||
- name: "Help"
|
||||
ignore_stdout: true
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
help
|
||||
|
||||
- name: "Topic-specific help"
|
||||
ignore_stdout: true
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
help echo
|
||||
@@ -0,0 +1,193 @@
|
||||
name: "Builtins: history"
|
||||
cases:
|
||||
- name: "basic history saving"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo something
|
||||
echo else
|
||||
|
||||
- name: "existing history file"
|
||||
pty: true
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
test_files:
|
||||
- path: "history-file.txt"
|
||||
contents: |
|
||||
a b c
|
||||
1 2 3
|
||||
ignore_stdout: true # Prompts and other don't quite line up
|
||||
stdin: |
|
||||
#expect-prompt
|
||||
history >history-output.txt
|
||||
#send:Enter
|
||||
#expect-prompt
|
||||
#send:Ctrl+D
|
||||
|
||||
- name: "existing history file with timestamps"
|
||||
pty: true
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
test_files:
|
||||
- path: "history-file.txt"
|
||||
contents: |
|
||||
#1750000000
|
||||
a b c
|
||||
#1750000001
|
||||
1 2 3
|
||||
ignore_stdout: true # Prompts and other don't quite line up
|
||||
stdin: |
|
||||
#expect-prompt
|
||||
history >history-output.txt
|
||||
#send:Enter
|
||||
#expect-prompt
|
||||
#send:Ctrl+D
|
||||
|
||||
- name: "existing history file with timestamps and HISTTIMEFORMAT"
|
||||
pty: true
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
HISTTIMEFORMAT: "%C "
|
||||
test_files:
|
||||
- path: "history-file.txt"
|
||||
contents: |
|
||||
#1750000000
|
||||
a b c
|
||||
#1750000001
|
||||
1 2 3
|
||||
ignore_stdout: true # Prompts and other don't quite line up
|
||||
stdin: |
|
||||
#expect-prompt
|
||||
#send:Ctrl+D
|
||||
|
||||
- name: "history N"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
history 2
|
||||
|
||||
- name: "history with duplicates"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd1
|
||||
: cmd1
|
||||
: cmd2
|
||||
: cmd1
|
||||
history
|
||||
|
||||
- name: "history -c"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
history -c
|
||||
history
|
||||
|
||||
- name: "history -d"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
history -d 1
|
||||
history
|
||||
|
||||
- name: "history -d: negative offset"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
history -d -2
|
||||
history
|
||||
|
||||
- name: "history -a"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo "[histfile]"
|
||||
cat $HISTFILE
|
||||
|
||||
history -a
|
||||
|
||||
echo "[histfile after -a]"
|
||||
cat $HISTFILE
|
||||
|
||||
history -a
|
||||
|
||||
echo "[histfile after second -a]"
|
||||
cat $HISTFILE
|
||||
|
||||
- name: "history -w"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
echo "[histfile]"
|
||||
cat $HISTFILE
|
||||
|
||||
history -w
|
||||
|
||||
echo "[histfile after -w]"
|
||||
cat $HISTFILE
|
||||
|
||||
history -w
|
||||
|
||||
echo "[histfile after second -w]"
|
||||
cat $HISTFILE
|
||||
|
||||
- name: "history -w with explicit file"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
history -w some-file.txt
|
||||
|
||||
- name: "history -s"
|
||||
known_failure: true # NOTE: bash excludes the history command itself from history
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
history -s a b c
|
||||
history
|
||||
|
||||
- name: "HISTCMD"
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
stdin: |
|
||||
: cmd1
|
||||
echo $HISTCMD
|
||||
: cmd2
|
||||
echo $HISTCMD
|
||||
: cmd3
|
||||
echo $HISTCMD
|
||||
|
||||
- name: "HISTTIMEFORMAT"
|
||||
known_failure: true # TODO(history): needs triage
|
||||
args: ["-o", "history"]
|
||||
env:
|
||||
HISTFILE: "history-file.txt"
|
||||
HISTTIMEFORMAT: "century=%C "
|
||||
stdin: |
|
||||
: cmd1
|
||||
: cmd2
|
||||
history
|
||||
|
||||
- name: "history file with unreadable lines"
|
||||
ignore_stderr: true # Ignore brush's warnings about unreadable lines
|
||||
stdin: |
|
||||
bash -c 'printf "\xff\xfe\xfd" >history-file.txt'
|
||||
HISTFILE=history-file.txt $0 -o history -c 'echo hi'
|
||||
@@ -0,0 +1,6 @@
|
||||
name: "Builtins: job-related builtins"
|
||||
cases:
|
||||
- name: "Basic async job"
|
||||
stdin: |
|
||||
echo hi &
|
||||
wait
|
||||
@@ -0,0 +1,27 @@
|
||||
name: "Builtins: kill"
|
||||
cases:
|
||||
- name: "kill -l"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
for i in $(seq 1 31); do kill -l $i; done
|
||||
# limit the number of signals to 31. Realtime signals are not implemented yet.
|
||||
for i in $(kill -l | sed -e "s/[[:digit:]]*)//g"); do echo $i; done | head -31
|
||||
# invalid option
|
||||
kill -l 9999
|
||||
kill -l HUP
|
||||
kill -l iNt
|
||||
kill -l int
|
||||
kill -l SIGHUP
|
||||
kill -l EXIT
|
||||
|
||||
- name: "kill -s"
|
||||
stdin: |
|
||||
kill -s USR1 $$
|
||||
|
||||
- name: "kill -n"
|
||||
stdin: |
|
||||
kill -n 9 $$
|
||||
|
||||
- name: "kill -sigspec"
|
||||
stdin: |
|
||||
kill -USR1 $$
|
||||
@@ -0,0 +1,28 @@
|
||||
name: "Builtins: let"
|
||||
cases:
|
||||
- name: "Basic let usage"
|
||||
stdin: |
|
||||
let 0; echo "0 => $?"
|
||||
let 1; echo "1 => $?"
|
||||
|
||||
let 0==0; echo "0==0 => $?"
|
||||
let 0!=0; echo "0!=0 => $?"
|
||||
|
||||
let 1 0; echo "1 0 => $?"
|
||||
let 0 1; echo "0 1 => $?"
|
||||
|
||||
- name: "let with assignment"
|
||||
stdin: |
|
||||
let x=10; echo "x=10 => $?; x==${x}"
|
||||
|
||||
- name: "let assignment through nameref"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
target=0
|
||||
declare -n ref=target
|
||||
let ref=42
|
||||
echo "target: $target"
|
||||
let ref+=8
|
||||
echo "target: $target"
|
||||
let ref++
|
||||
echo "target: $target"
|
||||
@@ -0,0 +1,21 @@
|
||||
name: "Builtins: local"
|
||||
cases:
|
||||
- name: "Basic local usage"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
local x=10
|
||||
echo "in myfunc: x==$x"
|
||||
}
|
||||
x=5
|
||||
echo "before call: x==$x"
|
||||
myfunc
|
||||
echo "after call: x==$x"
|
||||
|
||||
- name: "Local with empty array"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
local x=()
|
||||
declare -p x
|
||||
echo "x[0]: ${x[0]}"
|
||||
}
|
||||
myfunc
|
||||
@@ -0,0 +1,202 @@
|
||||
name: "Builtins: mapfile"
|
||||
cases:
|
||||
#
|
||||
# Basic functionality
|
||||
#
|
||||
- name: "mapfile basic"
|
||||
stdin: |
|
||||
mapfile arr < <(echo -e "a\nb\nc")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile -t"
|
||||
stdin: |
|
||||
mapfile -t arr < <(echo -e "a\nb\nc")
|
||||
declare -p arr
|
||||
|
||||
- name: "readarray -t"
|
||||
stdin: |
|
||||
readarray -t arr < <(echo -e "a\nb\nc")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile default MAPFILE"
|
||||
stdin: |
|
||||
mapfile -t < <(echo -e "a\nb")
|
||||
declare -p MAPFILE
|
||||
|
||||
#
|
||||
# -O flag
|
||||
#
|
||||
- name: "mapfile -O starts at offset"
|
||||
stdin: |
|
||||
mapfile -t -O 3 arr < <(echo -e "a\nb\nc")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile -O 0 preserves existing"
|
||||
stdin: |
|
||||
arr=(x y z w)
|
||||
mapfile -t -O 0 arr < <(echo -e "a\nb")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile without -O clears existing"
|
||||
stdin: |
|
||||
arr=(x y z w)
|
||||
mapfile -t arr < <(echo -e "a\nb")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile -O preserves sparse array"
|
||||
stdin: |
|
||||
declare -a arr
|
||||
arr[0]=x
|
||||
arr[5]=y
|
||||
arr[10]=z
|
||||
mapfile -t -O 2 arr < <(echo -e "a\nb")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile -O large offset"
|
||||
stdin: |
|
||||
mapfile -t -O 100 arr < <(echo -e "a\nb")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile -O negative"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
mapfile -O -1 arr < /dev/null
|
||||
echo "exit: $?"
|
||||
|
||||
#
|
||||
# Variable state edge cases
|
||||
#
|
||||
- name: "mapfile into nonexistent variable"
|
||||
stdin: |
|
||||
mapfile -t newvar < <(echo -e "a\nb")
|
||||
declare -p newvar
|
||||
|
||||
- name: "mapfile into string variable"
|
||||
stdin: |
|
||||
myvar="hello"
|
||||
mapfile -t myvar < <(echo -e "a\nb")
|
||||
declare -p myvar
|
||||
|
||||
- name: "mapfile -O into string variable"
|
||||
stdin: |
|
||||
myvar="hello"
|
||||
mapfile -t -O 2 myvar < <(echo -e "a\nb")
|
||||
declare -p myvar
|
||||
|
||||
- name: "mapfile into associative array"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
declare -A myassoc
|
||||
mapfile -t myassoc < <(echo -e "a\nb")
|
||||
echo "exit: $?"
|
||||
|
||||
#
|
||||
# -d delimiter
|
||||
#
|
||||
- name: "mapfile -d custom delimiter"
|
||||
stdin: |
|
||||
mapfile -t -d 'Z' arr < <(echo -ne "helloZthereZgeneralZkenobi")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile -d empty string (NUL)"
|
||||
stdin: |
|
||||
mapfile -t -d '' arr < <(printf 'a\0b\0c')
|
||||
declare -p arr
|
||||
|
||||
#
|
||||
# -n count
|
||||
#
|
||||
- name: "mapfile -n limits count"
|
||||
stdin: |
|
||||
mapfile -t -n 2 arr < <(echo -e "a\nb\nc\nd")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile -n 0 no limit"
|
||||
stdin: |
|
||||
mapfile -t -n 0 arr < <(echo -e "a\nb\nc")
|
||||
declare -p arr
|
||||
|
||||
#
|
||||
# -s skip
|
||||
#
|
||||
- name: "mapfile -s"
|
||||
stdin: |
|
||||
mapfile -t -s 2 arr < <(echo -e "a\nb\nc\nd")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile -s with -n"
|
||||
stdin: |
|
||||
mapfile -t -s 1 -n 2 arr < <(echo -e "a\nb\nc\nd\ne")
|
||||
declare -p arr
|
||||
|
||||
#
|
||||
# -u file descriptor
|
||||
#
|
||||
- name: "mapfile -u reads from fd"
|
||||
stdin: |
|
||||
exec 3< <(echo -e "a\nb\nc")
|
||||
mapfile -t -u 3 arr
|
||||
exec 3<&-
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile -u invalid fd"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
mapfile -u 99 arr
|
||||
echo "exit: $?"
|
||||
|
||||
#
|
||||
# Combinations
|
||||
#
|
||||
- name: "mapfile -O with -s and -n"
|
||||
stdin: |
|
||||
mapfile -t -O 10 -s 1 -n 2 arr < <(echo -e "a\nb\nc\nd\ne")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile multiple invocations without -O"
|
||||
stdin: |
|
||||
mapfile -t arr < <(echo -e "a\nb")
|
||||
mapfile -t arr < <(echo -e "x\ny\nz")
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile multiple invocations with -O"
|
||||
stdin: |
|
||||
mapfile -t arr < <(echo -e "a\nb")
|
||||
mapfile -t -O 2 arr < <(echo -e "x\ny\nz")
|
||||
declare -p arr
|
||||
|
||||
#
|
||||
# Empty input
|
||||
#
|
||||
- name: "mapfile empty input"
|
||||
stdin: |
|
||||
mapfile -t arr < /dev/null
|
||||
declare -p arr 2>/dev/null || echo "arr not set"
|
||||
|
||||
- name: "mapfile empty input clears existing"
|
||||
stdin: |
|
||||
arr=(x y z)
|
||||
mapfile -t arr < /dev/null
|
||||
declare -p arr 2>/dev/null || echo "arr not set"
|
||||
|
||||
- name: "mapfile -O empty input preserves"
|
||||
stdin: |
|
||||
arr=(x y z)
|
||||
mapfile -t -O 0 arr < /dev/null
|
||||
declare -p arr
|
||||
|
||||
- name: "mapfile into nameref variable"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
declare -a target
|
||||
declare -n ref=target
|
||||
mapfile -t ref < <(echo -e "a\nb\nc")
|
||||
declare -p target
|
||||
|
||||
- name: "mapfile -O into nameref variable"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
target=(x y)
|
||||
declare -n ref=target
|
||||
mapfile -t -O 2 ref < <(echo -e "a\nb")
|
||||
declare -p target
|
||||
@@ -0,0 +1,347 @@
|
||||
name: "Builtins: printf"
|
||||
|
||||
cases:
|
||||
# Basic functionality tests
|
||||
- name: "One-variable printf"
|
||||
stdin: |
|
||||
printf "%s" "0"
|
||||
|
||||
- name: "Basic printf"
|
||||
stdin: |
|
||||
printf "%s, %s" "Hello" "world"
|
||||
|
||||
- name: "printf -v"
|
||||
stdin: |
|
||||
printf -v myvar "%s, %s" "Hello" "world"
|
||||
echo "myvar: '${myvar}'"
|
||||
|
||||
- name: "printf -v with array index"
|
||||
stdin: |
|
||||
declare -a myarray=()
|
||||
printf -v 'myarray[5]' "%s, %s" "Hello" "world"
|
||||
declare | grep myarray
|
||||
|
||||
- name: "printf with -v as a format arg"
|
||||
stdin: |
|
||||
printf "%s\n" "-v"
|
||||
|
||||
# Bash-specific %q format (shell-safe quoting)
|
||||
- name: "printf %q"
|
||||
stdin: |
|
||||
echo "[1]"
|
||||
printf "%q" 'TEXT'; echo
|
||||
|
||||
echo "[2]"
|
||||
printf "%q" '$VAR'; echo
|
||||
|
||||
echo "[3]"
|
||||
printf "%q" '"'; echo
|
||||
|
||||
- name: "printf ~%q"
|
||||
stdin: |
|
||||
echo "[1]"
|
||||
printf "~%q" 'TEXT'; echo
|
||||
|
||||
echo "[2]"
|
||||
printf "~%q" '$VAR'; echo
|
||||
|
||||
echo "[3]"
|
||||
printf "~%q" '"'; echo
|
||||
|
||||
# =================================================================
|
||||
# STANDARD C PRINTF FORMAT SPECIFIERS
|
||||
# These should work with external printf and bash builtin printf
|
||||
# =================================================================
|
||||
|
||||
# Integer formatting tests
|
||||
- name: "printf %d with integers"
|
||||
stdin: |
|
||||
printf "%d\n" 42
|
||||
printf "%d\n" -17
|
||||
printf "%d\n" 0
|
||||
|
||||
- name: "printf %o (octal)"
|
||||
stdin: |
|
||||
printf "%o\n" 42
|
||||
printf "%o\n" 8
|
||||
printf "%o\n" 255
|
||||
|
||||
- name: "printf %x and %X (hex)"
|
||||
stdin: |
|
||||
printf "%x\n" 255
|
||||
printf "%X\n" 255
|
||||
printf "%x\n" 42
|
||||
|
||||
# Floating point tests
|
||||
- name: "printf %f (float)"
|
||||
stdin: |
|
||||
printf "%f\n" 3.14159
|
||||
printf "%.2f\n" 3.14159
|
||||
printf "%6.2f\n" 3.14159
|
||||
|
||||
- name: "printf %e and %E (scientific)"
|
||||
stdin: |
|
||||
printf "%e\n" 1234.5
|
||||
printf "%E\n" 1234.5
|
||||
|
||||
- name: "printf %g and %G (general)"
|
||||
stdin: |
|
||||
printf "%g\n" 1234.5
|
||||
printf "%G\n" 0.00012345
|
||||
|
||||
# Character formatting
|
||||
- name: "printf %c (character)"
|
||||
stdin: |
|
||||
printf "%c\n" 65
|
||||
printf "%c\n" 97
|
||||
printf "%c%c%c\n" 72 101 121
|
||||
|
||||
# Width and precision tests
|
||||
- name: "printf width formatting"
|
||||
stdin: |
|
||||
printf "%5s|\n" "hi"
|
||||
printf "%-5s|\n" "hi"
|
||||
printf "%05d|\n" 42
|
||||
|
||||
- name: "printf precision with strings"
|
||||
stdin: |
|
||||
printf "%.3s\n" "hello"
|
||||
printf "%10.3s\n" "hello"
|
||||
|
||||
# Multiple arguments and reuse
|
||||
- name: "printf argument reuse"
|
||||
stdin: |
|
||||
printf "%s %s %s\n" "one" "two"
|
||||
printf "%d %d %d\n" 1 2
|
||||
|
||||
# Special characters and escapes
|
||||
- name: "printf with escape sequences"
|
||||
stdin: |
|
||||
printf "hello\nworld\n"
|
||||
printf "tab\there\n"
|
||||
printf "quote: \"\n"
|
||||
|
||||
- name: "printf with backslash escapes"
|
||||
stdin: |
|
||||
printf "\\n\\t\\r\\\\\n"
|
||||
|
||||
# Missing arguments (bash handles gracefully)
|
||||
- name: "printf missing arguments"
|
||||
stdin: |
|
||||
printf "%s %s\n" "only_one"
|
||||
|
||||
# Empty and edge cases
|
||||
- name: "printf with empty format"
|
||||
stdin: |
|
||||
printf ""
|
||||
|
||||
- name: "printf with just format string"
|
||||
stdin: |
|
||||
printf "just text\n"
|
||||
|
||||
# Type conversion tests (safe cases only)
|
||||
- name: "printf valid string to number conversion"
|
||||
stdin: |
|
||||
printf "%d\n" "42"
|
||||
printf "%d\n" "0"
|
||||
|
||||
- name: "printf with percent literal"
|
||||
stdin: |
|
||||
printf "100%% complete\n"
|
||||
|
||||
- name: "printf %b (backslash escape expansion)"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
printf "%b\n" "hello\\nworld"
|
||||
printf "%b\n" "tab\\there"
|
||||
printf "%b\n" "quote\\\"here"
|
||||
|
||||
- name: "printf %b with field width"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
printf "%10b|\n" "hello\\nworld"
|
||||
printf "%-10b|\n" "tab\\there"
|
||||
|
||||
- name: "printf %T (date/time formatting)"
|
||||
known_failure: true # Not supported by uucore
|
||||
stdin: |
|
||||
printf "%(%Y-%m-%d)T\n" 1234567890
|
||||
|
||||
# Test argument reuse with bash-specific formats
|
||||
- name: "printf %q argument reuse"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
printf "%q %q %q\n" "one with spaces" "two"
|
||||
|
||||
- name: "printf %b argument reuse"
|
||||
stdin: |
|
||||
printf "%b %b %b\n" "one\\ntwo" "three"
|
||||
|
||||
# Test C-style constant interpretation
|
||||
- name: "printf with quoted character arguments"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
printf "%d\n" "'A"
|
||||
printf "%d\n" '"'
|
||||
|
||||
- name: "printf with plus/minus signs in numeric arguments"
|
||||
stdin: |
|
||||
printf "%d\n" "+42"
|
||||
printf "%d\n" "-17"
|
||||
|
||||
# =================================================================
|
||||
# ADDITIONAL STANDARD FORMAT SPECIFIERS
|
||||
# More comprehensive coverage of printf(3) man page features
|
||||
# =================================================================
|
||||
|
||||
# Additional format specifiers from man page
|
||||
- name: "printf %i (signed integer)"
|
||||
stdin: |
|
||||
printf "%i\n" 42
|
||||
printf "%i\n" -17
|
||||
|
||||
- name: "printf %u (unsigned integer)"
|
||||
stdin: |
|
||||
printf "%u\n" 42
|
||||
printf "%u\n" 4294967295
|
||||
|
||||
# Advanced width and precision
|
||||
- name: "printf with asterisk width and precision"
|
||||
stdin: |
|
||||
printf "%*s|\n" 10 "hello"
|
||||
printf "%.*s|\n" 3 "hello world"
|
||||
printf "%*.*s|\n" 10 3 "hello world"
|
||||
|
||||
- name: "printf with zero precision and zero arg"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
printf "%.0d\n" 0
|
||||
|
||||
- name: "printf with zero precision"
|
||||
stdin: |
|
||||
printf "%.0d\n" 42
|
||||
|
||||
# Flag characters
|
||||
- name: "printf with flag characters"
|
||||
stdin: |
|
||||
printf "%+d\n" 42
|
||||
printf "% d\n" 42
|
||||
printf "%#x\n" 255
|
||||
printf "%#o\n" 64
|
||||
|
||||
# Left justification
|
||||
- name: "printf left justification"
|
||||
stdin: |
|
||||
printf "%-10s|\n" "left"
|
||||
printf "%-10d|\n" 42
|
||||
|
||||
# Zero padding
|
||||
- name: "printf zero padding"
|
||||
stdin: |
|
||||
printf "%010d\n" 42
|
||||
printf "%010.2f\n" 3.14
|
||||
|
||||
# Combining flags
|
||||
- name: "printf combining flags"
|
||||
stdin: |
|
||||
printf "%+010d\n" 42
|
||||
printf "%-+10d|\n" 42
|
||||
|
||||
# More precision tests
|
||||
- name: "printf precision with integers"
|
||||
stdin: |
|
||||
printf "%.5d\n" 42
|
||||
printf "%.5o\n" 42
|
||||
printf "%.5x\n" 42
|
||||
|
||||
# Large numbers and edge cases
|
||||
- name: "printf with large numbers"
|
||||
stdin: |
|
||||
printf "%d\n" 2147483647
|
||||
printf "%u\n" 4294967295
|
||||
printf "%x\n" 4294967295
|
||||
|
||||
# More character and escape testing
|
||||
- name: "printf with special characters"
|
||||
stdin: |
|
||||
printf "%c\n" 0
|
||||
printf "%c\n" 32
|
||||
printf "%c\n" 127
|
||||
|
||||
# Negative numbers with different formats
|
||||
- name: "printf negative numbers"
|
||||
stdin: |
|
||||
printf "%d\n" -1
|
||||
printf "%o\n" -1
|
||||
printf "%x\n" -1
|
||||
printf "%u\n" -1
|
||||
|
||||
# Empty string handling
|
||||
- name: "printf with empty strings"
|
||||
stdin: |
|
||||
printf "%s|\n" ""
|
||||
printf "%5s|\n" ""
|
||||
printf "%.5s|\n" ""
|
||||
|
||||
# More empty string handling
|
||||
- name: "printf with empty strings for numbers"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
printf "%d" ""
|
||||
printf "%x" ""
|
||||
|
||||
# Scientific notation edge cases
|
||||
- name: "printf scientific notation edge cases"
|
||||
stdin: |
|
||||
printf "%e\n" 0.0
|
||||
printf "%E\n" 0.0
|
||||
printf "%g\n" 0.0000001
|
||||
printf "%G\n" 1000000.0
|
||||
|
||||
# Mixed format strings
|
||||
- name: "printf mixed formats in one string"
|
||||
stdin: |
|
||||
printf "Dec: %d, Hex: %x, Oct: %o, Char: %c\n" 65 65 65 65
|
||||
|
||||
# Field width larger than content
|
||||
- name: "printf wide fields"
|
||||
stdin: |
|
||||
printf "%20s|\n" "short"
|
||||
printf "%20d|\n" 42
|
||||
|
||||
# Return value testing (should always be 0 for successful printf)
|
||||
- name: "printf return value"
|
||||
stdin: |
|
||||
printf "test"
|
||||
echo " exit: $?"
|
||||
|
||||
# Error handling tests - marked as known failures due to external printf differences
|
||||
- name: "printf with invalid format"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
printf "%z\n" "test"
|
||||
|
||||
- name: "printf with no arguments"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
printf
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "printf string to number conversion with invalid input"
|
||||
known_failure: true # Need to investigate
|
||||
stdin: |
|
||||
printf "%d\n" "abc"
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "printf %q with empty string"
|
||||
stdin: |
|
||||
printf '%q\n' ''
|
||||
printf '~%q\n' ''
|
||||
|
||||
- name: "printf -v through nameref"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
declare -n ref=target
|
||||
printf -v ref "formatted %d" 42
|
||||
echo "target: $target"
|
||||
echo "ref: $ref"
|
||||
@@ -0,0 +1,8 @@
|
||||
name: "Builtins: PS4"
|
||||
cases:
|
||||
- name: "PS4 expansion"
|
||||
stdin: |
|
||||
PS4='+$FUNCNAME '
|
||||
bar() { true; }
|
||||
foo() { set -x; bar; set +x; }
|
||||
foo
|
||||
@@ -0,0 +1,65 @@
|
||||
name: "Builtins: pushd/popd/dirs"
|
||||
incompatible_configs: ["sh"]
|
||||
cases:
|
||||
- name: "Basic pushd usage"
|
||||
stdin: |
|
||||
cd /
|
||||
pushd /
|
||||
echo $?
|
||||
echo $PWD
|
||||
pushd /usr
|
||||
dirs
|
||||
echo $?
|
||||
echo $PWD
|
||||
popd
|
||||
echo $?
|
||||
echo $PWD
|
||||
popd
|
||||
echo $?
|
||||
|
||||
- name: "pushd without dir change"
|
||||
stdin: |
|
||||
cd /
|
||||
pushd -n /usr
|
||||
dirs
|
||||
|
||||
- name: "popd without dir change"
|
||||
stdin: |
|
||||
cd /
|
||||
pushd /
|
||||
pushd /usr
|
||||
popd -n
|
||||
dirs
|
||||
|
||||
- name: "popd with empty stack"
|
||||
ignore_stderr: true
|
||||
stdin: popd
|
||||
|
||||
- name: "pushd to non-existent dir"
|
||||
ignore_stderr: true
|
||||
stdin: pushd /non-existent-dir
|
||||
|
||||
- name: "basic dirs usage"
|
||||
stdin: |
|
||||
cd /
|
||||
dirs
|
||||
|
||||
- name: "dirs with tilde replacement"
|
||||
env:
|
||||
HOME: /usr
|
||||
stdin: |
|
||||
echo "HOME: $HOME"
|
||||
|
||||
cd ~
|
||||
echo "New PWD: $PWD"
|
||||
|
||||
dirs
|
||||
dirs -l
|
||||
|
||||
- name: "dirs to clear"
|
||||
stdin: |
|
||||
cd /usr
|
||||
pushd /usr
|
||||
pushd /
|
||||
dirs -c
|
||||
dirs
|
||||
@@ -0,0 +1,70 @@
|
||||
name: "Builtins: pwd"
|
||||
cases:
|
||||
- name: "Basic pwd usage"
|
||||
stdin: |
|
||||
cd /
|
||||
pwd
|
||||
echo "Result: $?"
|
||||
#
|
||||
cd usr
|
||||
pwd
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "pwd -LP"
|
||||
stdin: |
|
||||
mkdir -p ./level1/level2/level3
|
||||
cd level1
|
||||
ln -s ./level2/level3 ./symlink
|
||||
|
||||
(
|
||||
cd ./symlink
|
||||
basename $(pwd)
|
||||
basename $(pwd -L)
|
||||
basename $(pwd -P)
|
||||
)
|
||||
(
|
||||
cd ./symlink
|
||||
export PWD=
|
||||
basename $(pwd)
|
||||
basename $(pwd -L)
|
||||
basename $(pwd -P)
|
||||
)
|
||||
(
|
||||
cd ./symlink
|
||||
export PWD=
|
||||
# start a shell without $PWD
|
||||
(
|
||||
basename $(pwd)
|
||||
basename $(pwd -L)
|
||||
basename $(pwd -P)
|
||||
)
|
||||
)
|
||||
|
||||
cd ~
|
||||
pwd
|
||||
pwd -L
|
||||
pwd -P
|
||||
|
||||
- name: "pwd with moved dir"
|
||||
known_failure: true # Needs investigation
|
||||
stdin: |
|
||||
root=$(pwd)
|
||||
|
||||
mkdir -p ${root}/subdir
|
||||
cd ${root}/subdir
|
||||
mv ${root}/subdir ${root}/renamed
|
||||
|
||||
echo "pwd -L: $(basename $(pwd -L))"
|
||||
echo "pwd -P: $(basename $(pwd -P))"
|
||||
|
||||
- name: "pwd with removed dir"
|
||||
known_failure: true # Needs investigation
|
||||
stdin: |
|
||||
root=$(pwd)
|
||||
|
||||
mkdir -p ${root}/subdir
|
||||
cd ${root}/subdir
|
||||
rmdir ${root}/subdir
|
||||
|
||||
echo "pwd -L: $(basename $(pwd -L))"
|
||||
echo "pwd -P: $(basename $(pwd -P))"
|
||||
@@ -0,0 +1,392 @@
|
||||
name: "Builtins: read"
|
||||
cases:
|
||||
- name: "Basic read usage from file"
|
||||
test_files:
|
||||
- path: "data.txt"
|
||||
contents: |
|
||||
a 1
|
||||
b 2
|
||||
stdin: |
|
||||
while read name num; do echo "Hello, $name => $num"; done < data.txt
|
||||
echo "Exited loop; name='$name', num='$num'"
|
||||
|
||||
- name: "Basic read usage from file and no var name"
|
||||
test_files:
|
||||
- path: "data.txt"
|
||||
contents: |
|
||||
a 1
|
||||
b 2
|
||||
stdin: |
|
||||
while read; do echo "reply: '$REPLY'"; done < data.txt
|
||||
echo "Exited loop; reply='$REPLY'"
|
||||
|
||||
- name: "Basic read usage with empty file"
|
||||
stdin: |
|
||||
while read name num; do echo "Hello, $name => $num"; done < /dev/null
|
||||
declare -p name num
|
||||
|
||||
while read; do echo "reply: '$REPLY'"; done < /dev/null
|
||||
declare -p REPLY
|
||||
|
||||
while read -a arr; do declare -p arr; done < /dev/null
|
||||
declare -p arr
|
||||
|
||||
- name: "Basic read usage from pipe"
|
||||
stdin: |
|
||||
echo "1."
|
||||
(echo a; echo b) | while read name; do echo "Hello, $name"; done
|
||||
|
||||
echo "2."
|
||||
(echo "a b") | while read name; do echo "Hello, 1:$name REPLY:$REPLY"; done
|
||||
|
||||
echo "3."
|
||||
(echo "a b") | while read -a arr; do declare -p arr; done
|
||||
|
||||
- name: "read with closed stdin"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo 1 | read <&-
|
||||
echo "result: $?"
|
||||
|
||||
- name: "read from here string"
|
||||
stdin: |
|
||||
read myvar <<< "hello"
|
||||
echo "myvar: ${myvar}"
|
||||
|
||||
- name: "read from process substitution"
|
||||
stdin: |
|
||||
read myvar < <(echo hello)
|
||||
echo "myvar: ${myvar}"
|
||||
|
||||
- name: "read with custom IFS"
|
||||
stdin: |
|
||||
content=" a b c "
|
||||
while IFS= read line; do
|
||||
echo "LINE: '$line'"
|
||||
done <<<"${content}"
|
||||
|
||||
- name: "read text with tabs and custom IFS"
|
||||
stdin: |
|
||||
while IFS="" read myvar; do
|
||||
echo "myvar1: |${myvar}|"
|
||||
done < <(printf "a\tb\nc d\te\n")
|
||||
|
||||
- name: "read with empty entries"
|
||||
stdin: |
|
||||
(echo "x"; echo ""; echo "y"; echo ""; echo "") | while read line; do echo "LINE: '$line'"; done
|
||||
echo 'x,,y,z' | (IFS=',' read READ1 READ2 READ3 READ4; declare -p READ1; declare -p READ2; declare -p READ3; declare -p READ4)
|
||||
|
||||
- name: "read -a with empty lines"
|
||||
stdin: |
|
||||
(echo "hi"; echo ""; echo "there"; echo ""; echo "you") | (read -a READ; declare -p READ)
|
||||
(echo -e "hi\t\tthere\t\tyou") | (read -a READ -d $'\t'; declare -p READ)
|
||||
|
||||
- name: "read -a with empty interior field"
|
||||
stdin: |
|
||||
echo -e -n "hi||there||you" | (IFS='|' read -a READ; declare -p READ)
|
||||
echo -e -n "hi there you " | (IFS=' ' read -a READ; declare -p READ)
|
||||
echo -e -n "hi there you " | (read -a READ; declare -p READ)
|
||||
|
||||
- name: "read -a with empty leading field"
|
||||
stdin: |
|
||||
echo -e -n "|hi|there|you" | (IFS='|' read -a READ; declare -p READ)
|
||||
|
||||
- name: "read -a with empty trailing field"
|
||||
stdin: |
|
||||
echo -e -n "hi|there|you|" | (IFS='|' read -a READ; declare -p READ)
|
||||
|
||||
- name: "read -a with empty entries + empty delimiter"
|
||||
stdin: |
|
||||
(echo "hi"; echo ""; echo "there"; echo ""; echo "you") | (read -a READ -d ''; declare -p READ)
|
||||
echo -e -n "hi\n\nthere\n\nyou\n" | (read -a READ -d ''; declare -p READ)
|
||||
|
||||
- name: "read -a with empty entries + empty delimiter + custom IFS"
|
||||
stdin: |
|
||||
(echo 'x,,y,z'; echo 'w,v'; echo ''; echo ''; echo 'm') | (IFS=',' read -d "" -a READ; declare -p READ)
|
||||
|
||||
- name: "read with empty delimiter"
|
||||
min_oracle_version: 5.2 # \n renders differently in older shell versions
|
||||
stdin: |
|
||||
echo x | (read -d ""; declare -p REPLY)
|
||||
|
||||
# Timeout tests (read -t)
|
||||
- name: "read -t 0 with data available in pipe"
|
||||
# TODO(read):
|
||||
# This test is racy - poll(0) may check the pipe before echo's data arrives.
|
||||
# Bash consistently wins this race; our implementation sometimes doesn't.
|
||||
# Worth investigating.
|
||||
skip: true
|
||||
stdin: |
|
||||
echo "test" | { read -t 0; echo "exit: $?"; }
|
||||
|
||||
- name: "read -t 0 with no data (empty pipe)"
|
||||
skip: true # TODO(read): see above
|
||||
stdin: |
|
||||
# Closed pipe with no data
|
||||
: | { read -t 0; echo "exit: $?"; }
|
||||
|
||||
- name: "read -t with data immediately available"
|
||||
stdin: |
|
||||
echo "hello world" | { read -t 1 line; echo "exit: $?, line: '$line'"; }
|
||||
|
||||
- name: "read -t with fractional timeout and data available"
|
||||
stdin: |
|
||||
echo "test data" | { read -t 0.5 var; echo "exit: $?, var: '$var'"; }
|
||||
|
||||
- name: "read -t with regular file (always ready)"
|
||||
test_files:
|
||||
- path: "data.txt"
|
||||
contents: |
|
||||
line1
|
||||
line2
|
||||
stdin: |
|
||||
read -t 0 < data.txt; echo "exit: $?"
|
||||
read -t 1 line < data.txt; echo "exit: $?, line: '$line'"
|
||||
|
||||
- name: "read -t negative timeout is invalid"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
read -t -1
|
||||
echo "exit: $?"
|
||||
|
||||
- name: "read -t with -n option"
|
||||
stdin: |
|
||||
echo "abcdef" | { read -t 1 -n 3 var; echo "exit: $?, var: '$var'"; }
|
||||
|
||||
- name: "read -t 0 with here string"
|
||||
stdin: |
|
||||
read -t 0 <<< "hello"; echo "exit: $?"
|
||||
read -t 1 var <<< "hello"; echo "exit: $?, var: '$var'"
|
||||
|
||||
# Raw mode tests (read -r)
|
||||
- name: "read -r preserves backslashes"
|
||||
stdin: |
|
||||
echo 'hello\nworld' | { read -r line; echo "line: '$line'"; }
|
||||
echo 'path\\to\\file' | { read -r line; echo "line: '$line'"; }
|
||||
|
||||
- name: "read without -r processes escape sequences"
|
||||
stdin: |
|
||||
echo 'hello\nworld' | { read line; echo "line: '$line'"; }
|
||||
echo 'a\\b' | { read line; echo "line: '$line'"; }
|
||||
|
||||
- name: "read -r with trailing backslash"
|
||||
stdin: |
|
||||
printf 'line with trailing\\' | { read -r line; echo "line: '$line'"; }
|
||||
|
||||
- name: "read without -r line continuation"
|
||||
stdin: |
|
||||
# Backslash-newline continues to next line
|
||||
printf 'first\\\nsecond' | { read line; echo "line: '$line'"; }
|
||||
|
||||
- name: "read -r does not do line continuation"
|
||||
stdin: |
|
||||
printf 'first\\\nsecond' | { read -r line; echo "line: '$line'"; }
|
||||
|
||||
# -N vs -n option tests
|
||||
- name: "read -n respects delimiter"
|
||||
stdin: |
|
||||
# -n stops at delimiter even before char count
|
||||
echo "ab:cd" | { read -n 5 var; echo "var: '$var'"; }
|
||||
|
||||
- name: "read -N ignores delimiter"
|
||||
skip: true # TODO(read): test sometimes results in printf error
|
||||
stdin: |
|
||||
# -N reads exactly N chars, delimiter treated as regular char
|
||||
printf "ab:cd" | { read -N 4 var; echo "var: '$var'"; }
|
||||
|
||||
- name: "read -N does not split by IFS"
|
||||
stdin: |
|
||||
# -N should not split result by IFS
|
||||
printf "a b c" | { read -N 5 var; echo "var: '$var'"; }
|
||||
|
||||
- name: "read -N with array"
|
||||
stdin: |
|
||||
# With -a and -N, entire input goes to single array element
|
||||
printf "a b c" | { read -N 5 -a arr; declare -p arr; }
|
||||
|
||||
# Exit code tests
|
||||
- name: "read exit code on success"
|
||||
stdin: |
|
||||
echo "data" | { read var; echo "exit: $?"; }
|
||||
|
||||
- name: "read exit code on EOF"
|
||||
stdin: |
|
||||
# EOF returns non-zero
|
||||
read var < /dev/null; echo "exit: $?"
|
||||
|
||||
- name: "read exit code on EOF with partial data"
|
||||
stdin: |
|
||||
# EOF with partial data still returns non-zero but assigns
|
||||
printf "partial" | { read var; echo "exit: $?, var: '$var'"; }
|
||||
|
||||
# IFS edge cases
|
||||
- name: "read with IFS containing both whitespace and non-whitespace"
|
||||
stdin: |
|
||||
echo "a:b c:d" | (IFS=': ' read v1 v2 v3 v4; echo "v1='$v1' v2='$v2' v3='$v3' v4='$v4'")
|
||||
|
||||
- name: "read with consecutive non-whitespace delimiters"
|
||||
stdin: |
|
||||
echo "a::b" | (IFS=':' read v1 v2 v3; echo "v1='$v1' v2='$v2' v3='$v3'")
|
||||
|
||||
- name: "read assignment to fewer variables than fields"
|
||||
stdin: |
|
||||
echo "a b c d e" | { read v1 v2; echo "v1='$v1' v2='$v2'"; }
|
||||
|
||||
- name: "read assignment to more variables than fields"
|
||||
stdin: |
|
||||
echo "a b" | { read v1 v2 v3 v4; echo "v1='$v1' v2='$v2' v3='$v3' v4='$v4'"; }
|
||||
|
||||
- name: "read last variable preserves original delimiters"
|
||||
stdin: |
|
||||
# Last variable should get remainder with original delimiters, not re-joined with space
|
||||
echo "x:y:z:w" | (IFS=':' read a b; echo "a='$a' b='$b'")
|
||||
echo "x:y::z:w" | (IFS=':' read a b; echo "a='$a' b='$b'")
|
||||
echo "a:b c:d e" | (IFS=': ' read v1 v2; echo "v1='$v1' v2='$v2'")
|
||||
|
||||
- name: "read with consecutive delimiters at field boundary"
|
||||
stdin: |
|
||||
# When field limit causes boundary at consecutive delimiters,
|
||||
# extra delimiters should be preserved in remainder.
|
||||
echo "x::y" | (IFS=':' read a b; echo "a='$a' b='$b'")
|
||||
echo "x:::y" | (IFS=':' read a b; echo "a='$a' b='$b'")
|
||||
# Also test with more than 2 variables
|
||||
echo "a::b:c" | (IFS=':' read v1 v2 v3; echo "v1='$v1' v2='$v2' v3='$v3'")
|
||||
|
||||
- name: "read with empty IFS preserves entire line"
|
||||
stdin: |
|
||||
# Empty IFS means no splitting - entire line goes to first variable
|
||||
echo "a b c" | (IFS='' read v1 v2 v3; echo "v1='$v1' v2='$v2' v3='$v3'")
|
||||
echo "a:b:c" | (IFS='' read v1 v2; echo "v1='$v1' v2='$v2'")
|
||||
# With array
|
||||
echo "a b c" | (IFS='' read -a arr; declare -p arr)
|
||||
|
||||
- name: "read REPLY preserves leading and trailing whitespace"
|
||||
stdin: |
|
||||
# When no variable names given, REPLY should preserve whitespace
|
||||
read <<< ' hello world '
|
||||
echo "REPLY='$REPLY'"
|
||||
read <<< ' tab separated '
|
||||
echo "REPLY='$REPLY'"
|
||||
|
||||
- name: "read -a clears pre-existing array elements"
|
||||
stdin: |
|
||||
# Array should be cleared before assignment, not merged
|
||||
arr=(old1 old2 old3 old4 old5)
|
||||
read -a arr <<< 'new1 new2'
|
||||
declare -p arr
|
||||
|
||||
- name: "read -N still processes backslashes without -r"
|
||||
stdin: |
|
||||
# -N ignores delimiter but still does backslash processing
|
||||
printf 'a\\nb' | { read -N 4 var; echo "var='$var'"; }
|
||||
printf 'a\\\\b' | { read -N 4 var; echo "var='$var'"; }
|
||||
|
||||
- name: "read -r -N for truly raw fixed-length reads"
|
||||
stdin: |
|
||||
# -r -N should preserve backslashes exactly
|
||||
printf 'a\\nb' | { read -r -N 4 var; echo "var='$var'"; }
|
||||
printf 'a\\\\b' | { read -r -N 4 var; echo "var='$var'"; }
|
||||
|
||||
- name: "read -u with invalid file descriptor"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
read -u 99 var
|
||||
echo "exit: $?"
|
||||
|
||||
- name: "read into readonly variable"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
readonly myvar="original"
|
||||
echo "test" | read myvar
|
||||
echo "exit: $?, myvar='$myvar'"
|
||||
|
||||
- name: "read with mixed IFS into fewer variables"
|
||||
stdin: |
|
||||
# Complex IFS with fewer variables - verify remainder handling
|
||||
echo 'x: y : z : w' | (IFS=': ' read a b; echo "a='$a' b='$b'")
|
||||
|
||||
# Backslash escape counting tests (bash counts OUTPUT chars, not INPUT)
|
||||
- name: "read -n counts output chars after escape processing"
|
||||
stdin: |
|
||||
# Bash counts OUTPUT characters toward -n limit (after escape processing).
|
||||
# Input: a, \, b, c = 4 input bytes. With -n 3:
|
||||
# 'a' (output 1), '\b' → 'b' (output 2), 'c' (output 3) = "abc"
|
||||
printf 'a\\bc' | { read -n 3 var; echo "var='$var'"; }
|
||||
# Input: \, \, a, b = 4 input bytes. With -n 3:
|
||||
# '\\' → '\' (output 1), 'a' (output 2), 'b' (output 3) = "\ab"
|
||||
printf '\\\\ab' | { read -n 3 var; echo "var='$var'"; }
|
||||
|
||||
- name: "read -n with backslash-newline continuation"
|
||||
stdin: |
|
||||
# Backslash-newline is line continuation (consumes both, outputs nothing).
|
||||
# Input: a, b, \, \n, c = 5 input bytes. With -n 3:
|
||||
# 'a' (output 1), 'b' (output 2), '\n' → continuation, 'c' (output 3) = "abc"
|
||||
printf 'ab\\\nc' | { read -n 3 var; echo "var='$var'"; }
|
||||
|
||||
- name: "read trailing backslash discarded on EOF"
|
||||
stdin: |
|
||||
# Bash discards a pending backslash when EOF is reached
|
||||
printf 'ab\\' | { read var; echo "var='$var'"; }
|
||||
printf 'ab\\' | { read -n 5 var; echo "var='$var'"; }
|
||||
|
||||
# Prompt tests (read -p)
|
||||
- name: "read -p reads input correctly"
|
||||
stdin: |
|
||||
# Test that -p option reads input correctly (prompt display behavior
|
||||
# differs between bash and brush when stdin is not a terminal)
|
||||
read -p "Enter: " var <<< "hello"
|
||||
echo "var='$var'"
|
||||
|
||||
# File descriptor tests (read -u)
|
||||
- name: "read -u with valid file descriptor"
|
||||
stdin: |
|
||||
# Read from a specific file descriptor
|
||||
exec 3<<< "from fd 3"
|
||||
read -u 3 var
|
||||
echo "var='$var'"
|
||||
exec 3<&-
|
||||
|
||||
- name: "read -u combined with other options"
|
||||
stdin: |
|
||||
exec 4<<< "hello world test"
|
||||
read -u 4 -a arr
|
||||
declare -p arr
|
||||
exec 4<&-
|
||||
|
||||
# Silent mode tests (read -s)
|
||||
- name: "read -s with pipe input"
|
||||
stdin: |
|
||||
# Silent mode should still read input correctly (just not echo it)
|
||||
echo "secret" | { read -s var; echo "var='$var'"; }
|
||||
|
||||
- name: "read -s with multiple variables"
|
||||
stdin: |
|
||||
echo "a b c" | { read -s v1 v2 v3; echo "v1='$v1' v2='$v2' v3='$v3'"; }
|
||||
|
||||
- name: "read into nameref variable"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
declare -n ref=result
|
||||
read ref <<< "input_value"
|
||||
echo "result: $result"
|
||||
echo "ref: $ref"
|
||||
|
||||
- name: "read -a into nameref array"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
declare -n ref=myarr
|
||||
read -a ref <<< "one two three"
|
||||
echo "myarr[0]: ${myarr[0]}"
|
||||
echo "myarr[1]: ${myarr[1]}"
|
||||
echo "myarr[2]: ${myarr[2]}"
|
||||
echo "ref[1]: ${ref[1]}"
|
||||
|
||||
- name: "read into subscripted nameref"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
arr=(a b c)
|
||||
declare -n ref='arr[1]'
|
||||
read ref <<< "replaced"
|
||||
echo "arr[0]: ${arr[0]}"
|
||||
echo "arr[1]: ${arr[1]}"
|
||||
echo "arr[2]: ${arr[2]}"
|
||||
@@ -0,0 +1,53 @@
|
||||
name: "Builtins: readonly"
|
||||
cases:
|
||||
- name: "making var readonly"
|
||||
stdin: |
|
||||
my_var="value"
|
||||
readonly my_var
|
||||
|
||||
echo "Invoking declare -p..."
|
||||
declare -p my_var
|
||||
|
||||
- name: "using readonly with value"
|
||||
stdin: |
|
||||
readonly my_var="my_value"
|
||||
|
||||
echo "Invoking declare -p..."
|
||||
declare -p my_var
|
||||
|
||||
- name: "readonly array after append"
|
||||
stdin: |
|
||||
ARR=()
|
||||
f() {
|
||||
ARR+=("a" "b")
|
||||
readonly ARR
|
||||
echo "${#ARR[@]}"
|
||||
}
|
||||
f
|
||||
|
||||
- name: "readonly on nameref makes nameref readonly"
|
||||
known_failure: true
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
target="value"
|
||||
declare -n ref=target
|
||||
readonly ref
|
||||
declare -p ref
|
||||
declare -p target
|
||||
# Changing what ref points to should fail
|
||||
declare -n ref=other 2>/dev/null
|
||||
echo "exit: $?"
|
||||
# But target should still be writable
|
||||
target="new_value"
|
||||
echo "target: $target"
|
||||
|
||||
- name: "readonly target through nameref assignment"
|
||||
known_failure: true
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
target="value"
|
||||
readonly target
|
||||
declare -n ref=target
|
||||
ref="new_value" 2>/dev/null
|
||||
echo "exit: $?"
|
||||
echo "target: $target"
|
||||
@@ -0,0 +1,158 @@
|
||||
name: "Builtins: return"
|
||||
cases:
|
||||
- name: "Return outside function or script"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
return 42
|
||||
|
||||
- name: "Return outside function with stderr to /dev/full"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
# /dev/full may not be available on all platforms.
|
||||
[[ -e /dev/full ]] || exit 0
|
||||
return 2>/dev/full
|
||||
echo "exit: $?"
|
||||
|
||||
- name: "Return in directly invoked script"
|
||||
ignore_stderr: true
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
return 42
|
||||
echo "Got past return"
|
||||
args: ["./script.sh"]
|
||||
|
||||
- name: "Return from sourced script"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
return 42
|
||||
echo "Got past return"
|
||||
stdin: |
|
||||
source script.sh
|
||||
|
||||
- name: "Return from subshell"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
(return)
|
||||
echo "Got past subshell"
|
||||
exit 42
|
||||
|
||||
- name: "Return from nested sourced script"
|
||||
test_files:
|
||||
- path: "inner.sh"
|
||||
contents: |
|
||||
return 42
|
||||
echo "Got past inner return"
|
||||
- path: "outer.sh"
|
||||
contents: |
|
||||
source inner.sh
|
||||
echo "Got to end of outer script"
|
||||
stdin: |
|
||||
source outer.sh
|
||||
|
||||
- name: "Return in function"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
myfunc() {
|
||||
echo "In myfunc()"
|
||||
return 5
|
||||
echo "Should not get here"
|
||||
}
|
||||
echo "Calling myfunc()..."
|
||||
myfunc
|
||||
echo "Returned: $?"
|
||||
args: ["./script.sh"]
|
||||
|
||||
- name: "Return in for loop in function"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
for i in 1 2 3; do
|
||||
echo "In myfunc: $i"
|
||||
return 5
|
||||
done
|
||||
}
|
||||
|
||||
myfunc
|
||||
echo "Returned: $?"
|
||||
|
||||
- name: "Return in arithmetic for loop in function"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
for ((i=0; i < 5; i++)); do
|
||||
echo "In myfunc: $i"
|
||||
return 5
|
||||
done
|
||||
}
|
||||
|
||||
myfunc
|
||||
echo "Returned: $?"
|
||||
|
||||
- name: "Return in while loop in function"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
i=0
|
||||
while [[ $i -lt 5 ]]; do
|
||||
echo "In myfunc: $i"
|
||||
return 33
|
||||
i=$((i+1))
|
||||
done
|
||||
}
|
||||
|
||||
myfunc
|
||||
echo "Returned: $?"
|
||||
|
||||
- name: "Return in case"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
case 1 in
|
||||
1)
|
||||
echo "In case"
|
||||
return 5
|
||||
echo "Should not get here"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
myfunc
|
||||
echo "Returned: $?"
|
||||
|
||||
- name: "Return in brace group"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
{
|
||||
echo "In brace group"
|
||||
return 5
|
||||
echo "Should not get here"
|
||||
}
|
||||
}
|
||||
|
||||
myfunc
|
||||
echo "Returned: $?"
|
||||
|
||||
- name: "Return in and/or"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
echo "In and/or" && return 5 && echo "Should not get here"
|
||||
}
|
||||
|
||||
myfunc
|
||||
echo "Returned: $?"
|
||||
|
||||
- name: "Return from nested clauses"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
while (($#)); do
|
||||
case $1 in
|
||||
*)
|
||||
shift 2 || {
|
||||
echo "Returning"
|
||||
return 5
|
||||
}
|
||||
echo "Shifted and fell through"
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
myfunc a b c
|
||||
@@ -0,0 +1,103 @@
|
||||
name: "Builtins: set"
|
||||
cases:
|
||||
- name: "set with no args"
|
||||
stdin: |
|
||||
MYVARIABLE=VALUE
|
||||
set > set-output.txt
|
||||
grep MYVARIABLE set-output.txt
|
||||
|
||||
# Remove set-output.txt to avoid it being byte-for-byte compared.
|
||||
rm set-output.txt
|
||||
|
||||
- name: "Basic set usage"
|
||||
stdin: |
|
||||
set a b c d
|
||||
echo ${*}
|
||||
|
||||
- name: "set with options"
|
||||
stdin: |
|
||||
function dumpopts {
|
||||
# Dump the options
|
||||
echo "[Options: $1]"
|
||||
echo "set options: " $-
|
||||
shopt -p -o pipefail
|
||||
}
|
||||
|
||||
set -e -u -o pipefail
|
||||
dumpopts enabled
|
||||
echo '*: ' $*
|
||||
|
||||
set +e +u +o pipefail
|
||||
dumpopts disabled
|
||||
echo '*: ' $*
|
||||
|
||||
- name: "set with multiple combined options"
|
||||
stdin: |
|
||||
function dumpopts {
|
||||
# Dump the options
|
||||
echo "[Options: $1]"
|
||||
echo "set options: " $-
|
||||
shopt -p -o pipefail
|
||||
}
|
||||
|
||||
set -euo pipefail
|
||||
dumpopts enabled
|
||||
echo '$*: ' $*
|
||||
|
||||
set +euo pipefail
|
||||
dumpopts disabled
|
||||
echo '$*: ' $*
|
||||
|
||||
- name: "set clearing args"
|
||||
stdin: |
|
||||
set a b c
|
||||
echo ${*}
|
||||
set a
|
||||
echo ${*}
|
||||
|
||||
- name: "set with -"
|
||||
stdin: |
|
||||
set - a b c
|
||||
echo "args: " ${*}
|
||||
set -
|
||||
echo "args: " ${*}
|
||||
|
||||
- name: "set with --"
|
||||
stdin: |
|
||||
set -- a b c
|
||||
echo "args: " ${*}
|
||||
set --
|
||||
echo "args: " ${*}
|
||||
|
||||
- name: "set with option-looking args"
|
||||
stdin: |
|
||||
set -- a -v
|
||||
echo ${*}
|
||||
|
||||
set - a -v
|
||||
echo ${*}
|
||||
|
||||
set a -v
|
||||
echo ${*}
|
||||
|
||||
set -- a +x
|
||||
echo ${*}
|
||||
|
||||
set - a +x
|
||||
echo ${*}
|
||||
|
||||
set a +x
|
||||
echo ${*}
|
||||
|
||||
- name: "function-local set"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
function f() {
|
||||
local -
|
||||
set -x
|
||||
echo "In func"
|
||||
}
|
||||
|
||||
echo "Before func"
|
||||
f
|
||||
echo "After func"
|
||||
@@ -0,0 +1,77 @@
|
||||
name: "Builtins: shopt"
|
||||
cases:
|
||||
- name: "shopt defaults"
|
||||
ignore_whitespace: true # Spacing changes across versions
|
||||
min_oracle_version: 5.3 # Options changed in bash 5.3
|
||||
incompatible_os: ["fedora"] # Fedora enables "syslog-history" option
|
||||
stdin: |
|
||||
shopt | sort | grep -v extglob
|
||||
|
||||
- name: "shopt interactive defaults"
|
||||
ignore_whitespace: true # Spacing changes across versions
|
||||
min_oracle_version: 5.3 # Options changed in bash 5.3
|
||||
incompatible_os: ["fedora"] # Fedora enables "syslog-history" option
|
||||
pty: true
|
||||
args: ["-i", "-c", "shopt | sort | grep -v extglob"]
|
||||
|
||||
- name: "shopt -o defaults"
|
||||
ignore_whitespace: true # Spacing changes across versions
|
||||
min_oracle_version: 5.3 # Options changed in bash 5.3
|
||||
stdin: |
|
||||
shopt -o | sort
|
||||
|
||||
- name: "shopt -o interactive defaults"
|
||||
ignore_whitespace: true # Spacing changes across versions
|
||||
min_oracle_version: 5.3 # Options changed in bash 5.3
|
||||
pty: true
|
||||
args: ["-i", "-c", "shopt -o | sort | grep -v monitor"]
|
||||
|
||||
- name: "extglob defaults"
|
||||
known_failure: true # TODO(patterns): we force this setting on in our shell
|
||||
stdin: |
|
||||
shopt extglob
|
||||
|
||||
- name: "extglob interactive defaults"
|
||||
pty: true
|
||||
args: ["-i", "-c", "shopt extglob"]
|
||||
known_failure: true
|
||||
|
||||
- name: "shopt -o interactive monitor default"
|
||||
ignore_whitespace: true # Spacing changes across versions
|
||||
min_oracle_version: 5.3 # Options changed in bash 5.3
|
||||
pty: true
|
||||
args: ["-i", "-c", "shopt -o monitor"]
|
||||
|
||||
- name: "shopt toggle"
|
||||
ignore_whitespace: true # Spacing changes across versions
|
||||
stdin: |
|
||||
echo "Setting checkwinsize"
|
||||
shopt -s checkwinsize
|
||||
|
||||
echo "Displaying checkwinsize"
|
||||
shopt checkwinsize
|
||||
shopt -p checkwinsize
|
||||
|
||||
echo "Unsetting checkwinsize"
|
||||
shopt -u checkwinsize
|
||||
|
||||
echo "Displaying checkwinsize"
|
||||
shopt checkwinsize
|
||||
shopt -p checkwinsize
|
||||
|
||||
- name: "shopt -o usage"
|
||||
ignore_whitespace: true # Spacing changes across versions
|
||||
stdin: |
|
||||
echo "Setting emacs"
|
||||
shopt -o -s emacs
|
||||
|
||||
echo "Displaying emacs"
|
||||
shopt -o emacs
|
||||
shopt -o -p emacs
|
||||
|
||||
echo "Unsetting emacs"
|
||||
shopt -o -u emacs
|
||||
|
||||
echo "Displaying emacs"
|
||||
shopt -o emacs
|
||||
shopt -o -p emacs
|
||||
@@ -0,0 +1,357 @@
|
||||
name: "Builtins: test"
|
||||
cases:
|
||||
- name: "test: = operator"
|
||||
stdin: |
|
||||
shopt -u nocasematch
|
||||
test "ab" = "ab" && echo "ab = ab"
|
||||
test "ab" = "AB" && echo "ab = AB"
|
||||
test "ab" = "cd" && echo "ab = cd"
|
||||
test "ab" = "a?" && echo "ab = a?"
|
||||
|
||||
shopt -s nocasematch
|
||||
test "ab" = "ab" && echo "ab = ab"
|
||||
test "ab" = "AB" && echo "ab = AB"
|
||||
test "ab" = "cd" && echo "ab = cd"
|
||||
test "ab" = "a?" && echo "ab = a?"
|
||||
|
||||
- name: "test: == operator"
|
||||
stdin: |
|
||||
shopt -u nocasematch
|
||||
test "ab" == "ab" && echo "ab == ab"
|
||||
test "ab" == "AB" && echo "ab == AB"
|
||||
test "ab" == "cd" && echo "ab == cd"
|
||||
test "ab" == "a?" && echo "ab == a?"
|
||||
|
||||
shopt -s nocasematch
|
||||
test "ab" == "ab" && echo "ab == ab"
|
||||
test "ab" == "AB" && echo "ab == AB"
|
||||
test "ab" == "cd" && echo "ab == cd"
|
||||
test "ab" == "a?" && echo "ab == a?"
|
||||
|
||||
- name: "test: files refer to same device and inode"
|
||||
stdin: |
|
||||
[ /bin/sh -ef /bin/sh ] && echo "-ef correctly identified device and inode numbers"
|
||||
|
||||
[ ! /etc/os-release -ef /bin/sh ] && echo "-ef correctly identified device and inode numbers that do not match"
|
||||
|
||||
- name: "test: file is newer"
|
||||
stdin: |
|
||||
touch -d "2 hours ago" bar
|
||||
touch foo
|
||||
|
||||
[ foo -nt bar ] && echo "-nt correctly identified newer file"
|
||||
[ foo -nt foo ] && echo "-nt incorrectly identified file as newer than itself"
|
||||
[ foo -nt file_no_exists ] && echo "-nt correctly identified when file2 does not exist"
|
||||
|
||||
- name: "test: file is older"
|
||||
stdin: |
|
||||
touch -d "2 hours ago" foo
|
||||
touch bar
|
||||
|
||||
[ foo -ot bar ] && echo "-ot correctly identified older file"
|
||||
[ foo -ot foo ] && echo "-ot incorrectly identified file as older than itself"
|
||||
[ file_no_exists -ot foo ] && echo "-ot correctly identified when file1 does not exist"
|
||||
|
||||
- name: "test: parens"
|
||||
stdin: |
|
||||
test \( "a" = "a" \) && echo "Parens work"
|
||||
|
||||
- name: "test: -- string"
|
||||
stdin: |
|
||||
test --
|
||||
echo "Result(test): $?"
|
||||
|
||||
[ -- ]
|
||||
echo "Result([): $?"
|
||||
|
||||
- name: "test: invalid operators"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
test - -
|
||||
echo "Result: $?"
|
||||
|
||||
# String operators
|
||||
- name: "test: != operator"
|
||||
stdin: |
|
||||
test "ab" != "cd" && echo "ab != cd"
|
||||
test "ab" != "ab" && echo "ab != ab"
|
||||
test "" != "x" && echo "empty != x"
|
||||
|
||||
- name: "test: -z operator (zero length)"
|
||||
stdin: |
|
||||
test -z "" && echo "-z empty"
|
||||
test -z "x" && echo "-z x"
|
||||
unset VAR
|
||||
test -z "$VAR" && echo "-z unset"
|
||||
|
||||
- name: "test: -n operator (non-zero length)"
|
||||
stdin: |
|
||||
test -n "x" && echo "-n x"
|
||||
test -n "" && echo "-n empty"
|
||||
VAR="hello"
|
||||
test -n "$VAR" && echo "-n set"
|
||||
|
||||
- name: "test: < operator (string sort before)"
|
||||
stdin: |
|
||||
test "a" \< "b" && echo "a < b"
|
||||
test "b" \< "a" && echo "b < a"
|
||||
test "abc" \< "abd" && echo "abc < abd"
|
||||
test "a" \< "a" && echo "a < a"
|
||||
|
||||
- name: "test: > operator (string sort after)"
|
||||
stdin: |
|
||||
test "b" \> "a" && echo "b > a"
|
||||
test "a" \> "b" && echo "a > b"
|
||||
test "abd" \> "abc" && echo "abd > abc"
|
||||
test "a" \> "a" && echo "a > a"
|
||||
|
||||
# Arithmetic comparison operators
|
||||
- name: "test: -eq operator"
|
||||
stdin: |
|
||||
test 5 -eq 5 && echo "5 -eq 5"
|
||||
test 5 -eq 6 && echo "5 -eq 6"
|
||||
test -1 -eq -1 && echo "-1 -eq -1"
|
||||
test 0 -eq 0 && echo "0 -eq 0"
|
||||
|
||||
- name: "test: -ne operator"
|
||||
stdin: |
|
||||
test 5 -ne 6 && echo "5 -ne 6"
|
||||
test 5 -ne 5 && echo "5 -ne 5"
|
||||
test -1 -ne 1 && echo "-1 -ne 1"
|
||||
|
||||
- name: "test: -lt operator"
|
||||
stdin: |
|
||||
test 3 -lt 5 && echo "3 -lt 5"
|
||||
test 5 -lt 3 && echo "5 -lt 3"
|
||||
test 5 -lt 5 && echo "5 -lt 5"
|
||||
test -2 -lt -1 && echo "-2 -lt -1"
|
||||
|
||||
- name: "test: -le operator"
|
||||
stdin: |
|
||||
test 3 -le 5 && echo "3 -le 5"
|
||||
test 5 -le 5 && echo "5 -le 5"
|
||||
test 6 -le 5 && echo "6 -le 5"
|
||||
test -2 -le -2 && echo "-2 -le -2"
|
||||
|
||||
- name: "test: -gt operator"
|
||||
stdin: |
|
||||
test 5 -gt 3 && echo "5 -gt 3"
|
||||
test 3 -gt 5 && echo "3 -gt 5"
|
||||
test 5 -gt 5 && echo "5 -gt 5"
|
||||
test -1 -gt -2 && echo "-1 -gt -2"
|
||||
|
||||
- name: "test: -ge operator"
|
||||
stdin: |
|
||||
test 5 -ge 3 && echo "5 -ge 3"
|
||||
test 5 -ge 5 && echo "5 -ge 5"
|
||||
test 3 -ge 5 && echo "3 -ge 5"
|
||||
test -1 -ge -1 && echo "-1 -ge -1"
|
||||
|
||||
# File existence and type operators
|
||||
- name: "test: -e operator (file exists)"
|
||||
stdin: |
|
||||
touch testfile
|
||||
test -e testfile && echo "-e existing file"
|
||||
test -e nonexistent && echo "-e nonexistent"
|
||||
mkdir testdir
|
||||
test -e testdir && echo "-e existing dir"
|
||||
|
||||
- name: "test: -a operator (file exists, deprecated)"
|
||||
stdin: |
|
||||
touch testfile
|
||||
test -a testfile && echo "-a existing file"
|
||||
test -a nonexistent && echo "-a nonexistent"
|
||||
|
||||
- name: "test: -f operator (regular file)"
|
||||
stdin: |
|
||||
touch testfile
|
||||
test -f testfile && echo "-f regular file"
|
||||
mkdir testdir
|
||||
test -f testdir && echo "-f directory"
|
||||
test -f nonexistent && echo "-f nonexistent"
|
||||
|
||||
- name: "test: -d operator (directory)"
|
||||
stdin: |
|
||||
mkdir testdir
|
||||
test -d testdir && echo "-d directory"
|
||||
touch testfile
|
||||
test -d testfile && echo "-d regular file"
|
||||
test -d nonexistent && echo "-d nonexistent"
|
||||
|
||||
- name: "test: -h and -L operators (symbolic link)"
|
||||
stdin: |
|
||||
touch target
|
||||
ln -s target symlink
|
||||
test -h symlink && echo "-h symlink"
|
||||
test -L symlink && echo "-L symlink"
|
||||
test -h target && echo "-h regular file"
|
||||
test -L target && echo "-L regular file"
|
||||
test -h nonexistent && echo "-h nonexistent"
|
||||
|
||||
- name: "test: -p operator (named pipe/FIFO)"
|
||||
stdin: |
|
||||
touch testfile
|
||||
test -p testfile && echo "-p regular file"
|
||||
test -p nonexistent && echo "-p nonexistent"
|
||||
|
||||
- name: "test: -S operator (socket)"
|
||||
stdin: |
|
||||
touch testfile
|
||||
test -S testfile && echo "-S regular file"
|
||||
test -S nonexistent && echo "-S nonexistent"
|
||||
|
||||
- name: "test: -b operator (block device)"
|
||||
stdin: |
|
||||
touch testfile
|
||||
test -b testfile && echo "-b regular file"
|
||||
test -b nonexistent && echo "-b nonexistent"
|
||||
# Block devices exist in /dev but may vary by system
|
||||
|
||||
- name: "test: -c operator (character device)"
|
||||
stdin: |
|
||||
touch testfile
|
||||
test -c testfile && echo "-c regular file"
|
||||
test -c nonexistent && echo "-c nonexistent"
|
||||
|
||||
# File permission operators
|
||||
- name: "test: -r operator (readable)"
|
||||
stdin: |
|
||||
touch testfile
|
||||
test -r testfile && echo "-r readable"
|
||||
test -r nonexistent && echo "-r nonexistent"
|
||||
|
||||
- name: "test: -w operator (writable)"
|
||||
stdin: |
|
||||
touch testfile
|
||||
test -w testfile && echo "-w writable"
|
||||
test -w nonexistent && echo "-w nonexistent"
|
||||
|
||||
- name: "test: -x operator (executable)"
|
||||
stdin: |
|
||||
touch testfile
|
||||
test -x testfile && echo "-x executable"
|
||||
|
||||
- name: "test: -s operator (size greater than zero)"
|
||||
stdin: |
|
||||
echo "content" > nonempty
|
||||
test -s nonempty && echo "-s nonempty"
|
||||
touch empty
|
||||
test -s empty && echo "-s empty"
|
||||
test -s nonexistent && echo "-s nonexistent"
|
||||
|
||||
# Variable and option operators
|
||||
- name: "test: -v operator (variable is set)"
|
||||
stdin: |
|
||||
VAR="hello"
|
||||
test -v VAR && echo "-v set variable"
|
||||
unset VAR
|
||||
test -v VAR && echo "-v unset variable"
|
||||
VAR=""
|
||||
test -v VAR && echo "-v empty variable"
|
||||
|
||||
- name: "test: -R operator (nameref variable)"
|
||||
stdin: |
|
||||
VAR="value"
|
||||
declare -n REF=VAR
|
||||
test -R REF && echo "-R nameref"
|
||||
test -R VAR && echo "-R regular var"
|
||||
unset REF VAR
|
||||
|
||||
- name: "test: -o operator (shell option)"
|
||||
stdin: |
|
||||
set -e
|
||||
test -o errexit && echo "-o errexit enabled"
|
||||
set +e
|
||||
test -o errexit && echo "-o errexit disabled"
|
||||
test -o nonexistentoption && echo "-o nonexistent"
|
||||
|
||||
# Logical operators
|
||||
- name: "test: ! operator (negation)"
|
||||
stdin: |
|
||||
test ! "a" = "b" && echo "! a=b"
|
||||
test ! "a" = "a" && echo "! a=a"
|
||||
test ! -e nonexistent && echo "! -e nonexistent"
|
||||
test ! -e / && echo "! -e /"
|
||||
|
||||
- name: "test: -a operator (logical AND)"
|
||||
stdin: |
|
||||
test "a" = "a" -a "b" = "b" && echo "true -a true"
|
||||
test "a" = "a" -a "b" = "c" && echo "true -a false"
|
||||
test "a" = "b" -a "c" = "c" && echo "false -a true"
|
||||
test "a" = "b" -a "c" = "d" && echo "false -a false"
|
||||
|
||||
- name: "test: -o operator (logical OR)"
|
||||
stdin: |
|
||||
test "a" = "a" -o "b" = "b" && echo "true -o true"
|
||||
test "a" = "a" -o "b" = "c" && echo "true -o false"
|
||||
test "a" = "b" -o "c" = "c" && echo "false -o true"
|
||||
test "a" = "b" -o "c" = "d" && echo "false -o false"
|
||||
|
||||
- name: "test: complex logical expressions"
|
||||
stdin: |
|
||||
test \( "a" = "a" -o "b" = "c" \) -a "d" = "d" && echo "complex 1"
|
||||
test ! \( "a" = "b" \) && echo "complex 2"
|
||||
test "a" = "a" -a \( "b" = "b" -o "c" = "d" \) && echo "complex 3"
|
||||
|
||||
# Edge cases
|
||||
- name: "test: single argument (non-empty string is true)"
|
||||
stdin: |
|
||||
test "hello" && echo "non-empty is true"
|
||||
test "" && echo "empty is true"
|
||||
test "0" && echo "zero string is true"
|
||||
|
||||
- name: "test: no arguments"
|
||||
stdin: |
|
||||
test
|
||||
echo "no args: $?"
|
||||
|
||||
- name: "test: special characters in strings"
|
||||
stdin: |
|
||||
test "hello world" = "hello world" && echo "spaces match"
|
||||
test "a b" = "a b" && echo "tabs match"
|
||||
test 'a"b' = 'a"b' && echo "quotes match"
|
||||
test "a'b" = "a'b" && echo "single quotes match"
|
||||
|
||||
- name: "test: arithmetic comparison with spaces/tabs in operand"
|
||||
stdin: |
|
||||
# Leading/trailing spaces
|
||||
test 0 -eq ' 0'
|
||||
echo "leading space: $?"
|
||||
|
||||
test 0 -eq '0 '
|
||||
echo "trailing space: $?"
|
||||
|
||||
test 0 -eq ' 0 '
|
||||
echo "both spaces: $?"
|
||||
|
||||
# Tabs
|
||||
test 0 -eq ' 0'
|
||||
echo "leading tab: $?"
|
||||
|
||||
test 0 -eq '0 '
|
||||
echo "trailing tab: $?"
|
||||
|
||||
- name: "test: arithmetic comparison with newline in operand"
|
||||
min_oracle_version: "5.3"
|
||||
stdin: |
|
||||
# Newline trimming seems to have changed in or around bash 5.3
|
||||
test 0 -eq ' 0
|
||||
'
|
||||
echo "newline trailing: $?"
|
||||
|
||||
- name: "test: -t operator with whitespace in fd"
|
||||
stdin: |
|
||||
# -t operator should trim whitespace before parsing fd number
|
||||
# These should all behave identically (false since not a terminal)
|
||||
test -t 0
|
||||
r1=$?
|
||||
test -t ' 0'
|
||||
r2=$?
|
||||
test -t '0 '
|
||||
r3=$?
|
||||
test -t ' 0 '
|
||||
r4=$?
|
||||
|
||||
# All should have the same result
|
||||
[ $r1 -eq $r2 ] && [ $r2 -eq $r3 ] && [ $r3 -eq $r4 ] && echo "All -t results match"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
name: "Builtins: times"
|
||||
cases:
|
||||
- name: "Basic usage"
|
||||
ignore_stdout: true
|
||||
stdin: |
|
||||
times
|
||||
@@ -0,0 +1,724 @@
|
||||
name: "Builtins: trap"
|
||||
cases:
|
||||
#
|
||||
# trap -l signal listing
|
||||
#
|
||||
- name: "trap -l - lists all signal names"
|
||||
stdin: |
|
||||
# Just verify it outputs something reasonable - check common signals exist
|
||||
trap -l | grep -qE 'HUP|SIGHUP' && echo "has HUP"
|
||||
trap -l | grep -qE 'INT|SIGINT' && echo "has INT"
|
||||
trap -l | grep -qE 'TERM|SIGTERM' && echo "has TERM"
|
||||
|
||||
#
|
||||
# Basic trap registration
|
||||
#
|
||||
- name: "trap registration"
|
||||
stdin: |
|
||||
trap "echo 1" SIGINT
|
||||
trap "echo 2" SIGINT
|
||||
trap -p INT
|
||||
|
||||
trap "echo 3" int
|
||||
trap -p INT
|
||||
|
||||
trap "echo 4" 2
|
||||
trap -p INT
|
||||
|
||||
- name: "trap unregistering"
|
||||
stdin: |
|
||||
echo "[Case 1]"
|
||||
trap "echo 1" SIGINT
|
||||
trap SIGINT
|
||||
trap -p INT
|
||||
|
||||
echo "[Case 2]"
|
||||
trap "echo 2" SIGINT
|
||||
trap - SIGINT
|
||||
trap -p INT
|
||||
|
||||
- name: "trap EXIT"
|
||||
stdin: |
|
||||
trap 'echo "[exit]"' EXIT
|
||||
trap -p EXIT
|
||||
|
||||
- name: "trap EXIT: status code handling"
|
||||
stdin: |
|
||||
trap 'echo "[exit]: \$?: $?"' EXIT
|
||||
trap -p EXIT
|
||||
false
|
||||
|
||||
- name: "trap DEBUG"
|
||||
stdin: |
|
||||
trap 'echo [command: ${BASH_COMMAND}]' DEBUG
|
||||
trap -p DEBUG
|
||||
|
||||
echo First
|
||||
echo Second
|
||||
false
|
||||
|
||||
- name: "trap ERR - registration only"
|
||||
stdin: |
|
||||
trap "echo [err]" ERR
|
||||
trap -p ERR
|
||||
|
||||
- name: "trap ERR - basic invocation"
|
||||
stdin: |
|
||||
trap 'echo "ERR: $?"' ERR
|
||||
false
|
||||
echo "continues"
|
||||
false
|
||||
echo "continues again"
|
||||
|
||||
- name: "trap ERR - not triggered in conditional"
|
||||
stdin: |
|
||||
trap 'echo "ERR trapped"' ERR
|
||||
if false; then
|
||||
echo "then"
|
||||
else
|
||||
echo "else"
|
||||
fi
|
||||
echo "after"
|
||||
|
||||
- name: "trap ERR - not triggered with && short-circuit"
|
||||
stdin: |
|
||||
trap 'echo "ERR trapped"' ERR
|
||||
false && echo "not printed"
|
||||
echo "after"
|
||||
|
||||
- name: "trap ERR - not triggered with ||"
|
||||
stdin: |
|
||||
trap 'echo "ERR trapped"' ERR
|
||||
false || echo "fallback"
|
||||
echo "after"
|
||||
|
||||
- name: "trap ERR - not triggered with negation"
|
||||
stdin: |
|
||||
trap 'echo "ERR trapped"' ERR
|
||||
! false
|
||||
echo "after"
|
||||
|
||||
- name: "trap ERR - not triggered in while condition"
|
||||
stdin: |
|
||||
trap 'echo "ERR"' ERR
|
||||
while false; do
|
||||
echo "loop body"
|
||||
done
|
||||
echo "after while"
|
||||
|
||||
- name: "trap ERR - not triggered in until condition"
|
||||
stdin: |
|
||||
trap 'echo "ERR"' ERR
|
||||
count=0
|
||||
until false; do
|
||||
((count++))
|
||||
if ((count > 2)); then break; fi
|
||||
done
|
||||
echo "after until"
|
||||
|
||||
- name: "trap ERR - not triggered as part of || list"
|
||||
stdin: |
|
||||
trap 'echo "ERR"' ERR
|
||||
false || true || false
|
||||
echo "after"
|
||||
|
||||
- name: "trap ERR - not triggered for negated command"
|
||||
stdin: |
|
||||
trap 'echo "ERR"' ERR
|
||||
if ! true; then
|
||||
echo "then"
|
||||
fi
|
||||
! false
|
||||
echo "after"
|
||||
|
||||
- name: "trap ERR - triggered after && chain completes then fails"
|
||||
stdin: |
|
||||
trap 'echo "ERR trapped"' ERR
|
||||
true && true
|
||||
false
|
||||
echo "after"
|
||||
|
||||
- name: "trap ERR - execution doesn't affect control flow"
|
||||
stdin: |
|
||||
trap 'echo "ERR"' ERR
|
||||
result=0
|
||||
false || result=$?
|
||||
echo "result: $result"
|
||||
|
||||
- name: "trap ERR - multiple failures trigger multiple traps"
|
||||
stdin: |
|
||||
count=0
|
||||
trap '((count++)); echo "ERR $count"' ERR
|
||||
false
|
||||
false
|
||||
false
|
||||
echo "total: $count"
|
||||
|
||||
- name: "trap ERR - has access to exit status"
|
||||
stdin: |
|
||||
trap 'echo "ERR with status: $?"' ERR
|
||||
(exit 42)
|
||||
echo "after"
|
||||
|
||||
- name: "trap ERR - has access to BASH_COMMAND"
|
||||
known_failure: true # TODO(traps): ERR trap basic execution not implemented
|
||||
stdin: |
|
||||
trap 'echo "ERR for: $BASH_COMMAND"' ERR
|
||||
nonexistent_command_xyz 2>/dev/null
|
||||
echo "after"
|
||||
|
||||
- name: "trap ERR - has access to LINENO"
|
||||
known_failure: true # TODO(traps): ERR trap basic execution not implemented
|
||||
stdin: |
|
||||
trap 'echo "ERR at line: $LINENO"' ERR
|
||||
false
|
||||
echo "after"
|
||||
|
||||
- name: "return in EXIT trap within function"
|
||||
known_failure: true # TODO(traps): return in trap handler behavior differs
|
||||
stdin: |
|
||||
myfunc() {
|
||||
trap 'return 0' EXIT
|
||||
false
|
||||
echo "should not print in func"
|
||||
}
|
||||
myfunc
|
||||
echo "after func"
|
||||
|
||||
- name: "subshell inherits signal traps (for trap -p display)"
|
||||
stdin: |
|
||||
trap 'echo "[parent int]"' INT
|
||||
trap 'echo "[parent term]"' TERM
|
||||
trap -p INT
|
||||
echo "---"
|
||||
(trap -p INT; trap -p TERM)
|
||||
|
||||
- name: "subshell can override inherited trap"
|
||||
stdin: |
|
||||
trap 'echo "[parent int]"' INT
|
||||
trap -p INT
|
||||
echo "---"
|
||||
(trap 'echo "[subshell int]"' INT; trap -p INT)
|
||||
|
||||
- name: "subshell inherits EXIT trap"
|
||||
stdin: |
|
||||
trap 'echo "[exit]"' EXIT
|
||||
trap -p EXIT
|
||||
echo "---"
|
||||
(trap -p EXIT)
|
||||
|
||||
- name: "subshell inherits DEBUG trap"
|
||||
stdin: |
|
||||
trap 'echo "[debug]"' DEBUG
|
||||
trap -p DEBUG
|
||||
echo "---"
|
||||
(trap -p DEBUG)
|
||||
|
||||
- name: "subshell does not execute inherited DEBUG trap"
|
||||
stdin: |
|
||||
trap 'echo "[debug]"' DEBUG
|
||||
echo "parent"
|
||||
echo "---"
|
||||
(echo "subshell")
|
||||
echo "back in parent"
|
||||
|
||||
- name: "subshell can set its own EXIT trap"
|
||||
known_failure: true # TODO(traps): EXIT trap in subshell
|
||||
stdin: |
|
||||
trap 'echo "[parent exit]"' EXIT
|
||||
echo "parent"
|
||||
(trap 'echo "[subshell exit]"' EXIT; echo "subshell")
|
||||
echo "back in parent"
|
||||
|
||||
- name: "subshell inherited EXIT trap does not fire"
|
||||
stdin: |
|
||||
trap 'echo "[exit]"' EXIT
|
||||
echo "parent"
|
||||
(echo "subshell")
|
||||
echo "back in parent"
|
||||
|
||||
#
|
||||
# Subshell trap inheritance in command substitution
|
||||
#
|
||||
- name: "command substitution inherits signal trap (trap -p display)"
|
||||
stdin: |
|
||||
trap 'echo "[int]"' INT
|
||||
result=$(trap -p INT)
|
||||
echo "$result"
|
||||
|
||||
- name: "command substitution does not fire inherited signal trap"
|
||||
known_failure: true # TODO(signals): signal handling in command substitution
|
||||
stdin: |
|
||||
trap 'echo "[int]" >&2' INT
|
||||
result=$(kill -INT $$; echo "after kill")
|
||||
echo "result: $result"
|
||||
|
||||
- name: "command substitution can set its own trap"
|
||||
stdin: |
|
||||
trap 'echo "[parent int]"' INT
|
||||
result=$(trap 'echo "[subshell int]"' INT; trap -p INT)
|
||||
echo "$result"
|
||||
|
||||
- name: "command substitution EXIT trap not inherited from parent"
|
||||
stdin: |
|
||||
trap 'echo "[parent exit]"' EXIT
|
||||
result=$(trap -p EXIT; echo "inside")
|
||||
echo "result: $result"
|
||||
|
||||
- name: "command substitution can have its own EXIT trap"
|
||||
known_failure: true # TODO: EXIT trap in command substitution subshell
|
||||
stdin: |
|
||||
trap 'echo "[parent exit]"' EXIT
|
||||
result=$(trap 'echo "[sub exit]"' EXIT; echo "inside")
|
||||
echo "result: $result"
|
||||
|
||||
- name: "nested command substitution trap inheritance"
|
||||
stdin: |
|
||||
trap 'echo "[int]"' INT
|
||||
outer=$(inner=$(trap -p INT); echo "inner: $inner")
|
||||
echo "outer: $outer"
|
||||
|
||||
#
|
||||
# Subshell trap inheritance in pipelines
|
||||
#
|
||||
- name: "pipeline component inherits signal trap (trap -p display)"
|
||||
stdin: |
|
||||
trap 'echo "[int]"' INT
|
||||
trap -p INT | cat
|
||||
|
||||
- name: "pipeline component can override trap"
|
||||
stdin: |
|
||||
trap 'echo "[parent int]"' INT
|
||||
{ trap 'echo "[pipe int]"' INT; trap -p INT; } | cat
|
||||
|
||||
- name: "pipeline EXIT trap behavior"
|
||||
stdin: |
|
||||
trap 'echo "[exit]"' EXIT
|
||||
{ trap -p EXIT; echo "in pipe"; } | cat
|
||||
echo "after pipe"
|
||||
|
||||
- name: "multiple pipeline stages trap inheritance"
|
||||
stdin: |
|
||||
trap 'echo "[int]"' INT
|
||||
trap -p INT | cat | cat
|
||||
|
||||
#
|
||||
# Subshell trap inheritance in process substitution
|
||||
#
|
||||
- name: "process substitution inherits signal trap"
|
||||
known_failure: true # TODO: process substitution trap inheritance
|
||||
stdin: |
|
||||
trap 'echo "[int]"' INT
|
||||
cat <(trap -p INT)
|
||||
|
||||
- name: "process substitution output trap inheritance"
|
||||
skip: true # TODO: process substitution trap inheritance / inconsistent across distros
|
||||
stdin: |
|
||||
trap 'echo "[int]"' INT
|
||||
echo "data" > >(trap -p INT; cat > /dev/null)
|
||||
# Give time for the process substitution to complete
|
||||
wait
|
||||
|
||||
- name: "process substitution can set own EXIT trap"
|
||||
known_failure: true # TODO: EXIT trap in process substitution
|
||||
stdin: |
|
||||
trap 'echo "[parent exit]"' EXIT
|
||||
cat <(trap 'echo "[sub exit]"' EXIT; echo "inside")
|
||||
echo "after"
|
||||
|
||||
#
|
||||
# Subshell trap inheritance in background jobs
|
||||
#
|
||||
- name: "background job inherits signal trap (trap -p display)"
|
||||
stdin: |
|
||||
trap 'echo "[int]"' INT
|
||||
trap -p INT &
|
||||
wait
|
||||
|
||||
- name: "background job does not fire inherited signal trap"
|
||||
known_failure: true # TODO(signals): background job signal handling
|
||||
stdin: |
|
||||
trap 'echo "[int]" >&2' INT
|
||||
{ kill -INT $$; echo "after kill"; } &
|
||||
wait
|
||||
echo "main done"
|
||||
|
||||
- name: "background job EXIT trap behavior"
|
||||
stdin: |
|
||||
trap 'echo "[exit]"' EXIT
|
||||
{ trap -p EXIT; echo "bg job"; } &
|
||||
wait
|
||||
echo "main continues"
|
||||
|
||||
- name: "background job can set own traps"
|
||||
stdin: |
|
||||
trap 'echo "[parent int]"' INT
|
||||
{ trap 'echo "[bg int]"' INT; trap -p INT; } &
|
||||
wait
|
||||
trap -p INT
|
||||
|
||||
#
|
||||
# Coproc trap inheritance (all coproc tests consolidated here)
|
||||
#
|
||||
# Note: "coproc - inherits signal trap" test is skipped due to bash version
|
||||
# differences. Older bash resets traps in coproc, newer bash (5.3.9) inherits them.
|
||||
# This causes inconsistent test results across CI runners.
|
||||
- name: "coproc - EXIT trap behavior"
|
||||
skip: true # TODO: this test is too inconsistent
|
||||
min_oracle_version: "5.3.0"
|
||||
incompatible_os:
|
||||
- fedora
|
||||
- arch
|
||||
stdin: |
|
||||
trap 'echo "[exit]"' EXIT
|
||||
coproc { trap -p EXIT; echo "coproc"; }
|
||||
cat <&${COPROC[0]}
|
||||
wait
|
||||
echo "main continues"
|
||||
|
||||
- name: "coproc - DEBUG trap inheritance"
|
||||
skip: true # TODO: this test is too inconsistent
|
||||
min_oracle_version: "5.3.0"
|
||||
incompatible_os:
|
||||
- fedora
|
||||
stdin: |
|
||||
trap 'echo "[debug]"' DEBUG
|
||||
coproc { trap -p DEBUG; echo "coproc"; }
|
||||
cat <&${COPROC[0]}
|
||||
wait
|
||||
|
||||
- name: "coproc - ERR trap with errtrace"
|
||||
skip: true # TODO: this test is too inconsistent
|
||||
min_oracle_version: "5.3.0"
|
||||
incompatible_os:
|
||||
- fedora
|
||||
- arch
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "[err]"' ERR
|
||||
coproc { trap -p ERR; false; echo "after false"; }
|
||||
cat <&${COPROC[0]}
|
||||
wait
|
||||
|
||||
#
|
||||
# RETURN trap - basic behavior
|
||||
#
|
||||
- name: "RETURN trap - fires on function return"
|
||||
stdin: |
|
||||
trap 'echo "[return]"' RETURN
|
||||
myfunc() {
|
||||
echo "in func"
|
||||
}
|
||||
myfunc
|
||||
echo "after func"
|
||||
|
||||
- name: "RETURN trap - fires with return value"
|
||||
stdin: |
|
||||
trap 'echo "[return: $?]"' RETURN
|
||||
myfunc() {
|
||||
echo "in func"
|
||||
return 42
|
||||
}
|
||||
myfunc
|
||||
echo "after func"
|
||||
|
||||
- name: "RETURN trap - fires on sourced script completion"
|
||||
known_failure: true # TODO(traps): RETURN trap not firing for sourced scripts
|
||||
test_files:
|
||||
- path: "sourced.sh"
|
||||
contents: |
|
||||
echo "in sourced"
|
||||
stdin: |
|
||||
trap 'echo "[return]"' RETURN
|
||||
. ./sourced.sh
|
||||
echo "after source"
|
||||
|
||||
- name: "RETURN trap - not inherited in function without functrace"
|
||||
stdin: |
|
||||
set +T
|
||||
trap 'echo "[return]"' RETURN
|
||||
outer() {
|
||||
inner() {
|
||||
echo "in inner"
|
||||
}
|
||||
inner
|
||||
echo "after inner"
|
||||
}
|
||||
outer
|
||||
echo "done"
|
||||
|
||||
- name: "RETURN trap - inherited with functrace"
|
||||
known_failure: true # TODO(traps): RETURN trap inheritance with functrace not implemented
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[return: ${FUNCNAME[0]:-main}]"' RETURN
|
||||
outer() {
|
||||
inner() {
|
||||
echo "in inner"
|
||||
}
|
||||
inner
|
||||
echo "after inner"
|
||||
}
|
||||
outer
|
||||
echo "done"
|
||||
|
||||
- name: "RETURN trap - in explicit subshell"
|
||||
stdin: |
|
||||
trap 'echo "[return]"' RETURN
|
||||
(
|
||||
myfunc() { echo "in func"; }
|
||||
myfunc
|
||||
)
|
||||
echo "after subshell"
|
||||
|
||||
- name: "RETURN trap - does not fire for regular commands"
|
||||
stdin: |
|
||||
trap 'echo "[return]"' RETURN
|
||||
echo "cmd1"
|
||||
echo "cmd2"
|
||||
true
|
||||
echo "done"
|
||||
|
||||
- name: "RETURN trap - does not fire for subshell completion"
|
||||
stdin: |
|
||||
trap 'echo "[return]"' RETURN
|
||||
(echo "in subshell")
|
||||
echo "after subshell"
|
||||
|
||||
#
|
||||
# Signal trap execution verification
|
||||
#
|
||||
- name: "trap SIGUSR1"
|
||||
known_failure: true # TODO(signals): signal handling
|
||||
stdin: |
|
||||
trap 'echo "[usr1 received]"' USR1
|
||||
trap -p USR1
|
||||
kill -USR1 $$
|
||||
echo "after signal"
|
||||
|
||||
- name: "trap SIGUSR2"
|
||||
known_failure: true # TODO(signals): signal handling
|
||||
stdin: |
|
||||
trap 'echo "[usr2 received]"' SIGUSR2
|
||||
trap -p USR2
|
||||
kill -USR2 $$
|
||||
echo "after signal"
|
||||
|
||||
- name: "trap multiple signals with same handler"
|
||||
known_failure: true # TODO(signals): signal handling
|
||||
stdin: |
|
||||
trap 'echo "[signal received]"' USR1 USR2
|
||||
kill -USR1 $$
|
||||
kill -USR2 $$
|
||||
echo "done"
|
||||
|
||||
- name: "trap handler can inspect signal"
|
||||
known_failure: true # TODO(signals): signal handling
|
||||
stdin: |
|
||||
handler() {
|
||||
echo "handler called"
|
||||
}
|
||||
trap handler USR1
|
||||
kill -USR1 $$
|
||||
echo "after"
|
||||
|
||||
#
|
||||
# Trap reset and unset
|
||||
#
|
||||
- name: "trap - clears trap"
|
||||
stdin: |
|
||||
trap 'echo "[int]"' INT
|
||||
trap -p INT
|
||||
trap - INT
|
||||
trap -p INT
|
||||
|
||||
- name: "trap '' ignores signal"
|
||||
known_failure: true # TODO(signals): signal handling
|
||||
stdin: |
|
||||
trap '' INT
|
||||
trap -p INT
|
||||
kill -INT $$
|
||||
echo "survived"
|
||||
|
||||
- name: "trap with empty string can be cleared"
|
||||
stdin: |
|
||||
trap '' INT
|
||||
trap -p INT
|
||||
trap - INT
|
||||
trap -p INT
|
||||
|
||||
#
|
||||
# trap -p edge cases
|
||||
#
|
||||
- name: "trap -p - with no args shows all traps"
|
||||
stdin: |
|
||||
trap 'echo a' INT
|
||||
trap 'echo b' TERM
|
||||
trap 'echo c' EXIT
|
||||
trap -p | sort
|
||||
|
||||
- name: "trap -p - with specific signal"
|
||||
stdin: |
|
||||
trap 'echo "[int]"' INT
|
||||
trap 'echo "[term]"' TERM
|
||||
trap -p INT
|
||||
trap -p TERM
|
||||
|
||||
- name: "trap -p - with multiple signals"
|
||||
stdin: |
|
||||
trap 'echo "[int]"' INT
|
||||
trap 'echo "[term]"' TERM
|
||||
trap 'echo "[usr1]"' USR1
|
||||
trap -p INT TERM USR1
|
||||
|
||||
- name: "trap -p - for unset trap shows nothing"
|
||||
stdin: |
|
||||
trap -p INT
|
||||
echo "done"
|
||||
|
||||
- name: "trap -p - output is re-executable"
|
||||
stdin: |
|
||||
trap 'echo "hello world"' INT
|
||||
eval "$(trap -p INT)"
|
||||
trap -p INT
|
||||
|
||||
#
|
||||
# Trap handler quoting
|
||||
#
|
||||
- name: "trap handler - single quotes preserved"
|
||||
known_failure: true # TODO(traps): quote escaping in trap -p output differs
|
||||
stdin: |
|
||||
trap 'echo '\''quoted'\''' INT
|
||||
trap -p INT
|
||||
|
||||
- name: "trap handler - double quotes in handler"
|
||||
stdin: |
|
||||
trap 'echo "double quoted"' INT
|
||||
trap -p INT
|
||||
|
||||
- name: "trap handler - special characters"
|
||||
stdin: |
|
||||
trap 'echo $HOME; echo "done"' INT
|
||||
trap -p INT
|
||||
|
||||
- name: "trap handler - newlines in handler"
|
||||
stdin: |
|
||||
trap 'echo line1
|
||||
echo line2' INT
|
||||
trap -p INT
|
||||
|
||||
#
|
||||
# DEBUG trap - basic behavior
|
||||
#
|
||||
- name: "DEBUG trap - sees BASH_COMMAND"
|
||||
known_failure: true # TODO(BASH_COMMAND): not showing assignment, quotes differ
|
||||
stdin: |
|
||||
trap 'echo "cmd: $BASH_COMMAND"' DEBUG
|
||||
x=1
|
||||
echo "x is $x"
|
||||
|
||||
- name: "DEBUG trap - sees LINENO"
|
||||
known_failure: true # TODO(traps): LINENO in DEBUG trap not accurate
|
||||
stdin: |
|
||||
trap 'echo "line: $LINENO"' DEBUG
|
||||
echo first
|
||||
echo second
|
||||
|
||||
- name: "DEBUG trap - fires before each simple command"
|
||||
known_failure: true # TODO(traps): DEBUG trap not firing for trap command itself
|
||||
stdin: |
|
||||
count=0
|
||||
trap '((count++))' DEBUG
|
||||
echo a
|
||||
echo b
|
||||
echo c
|
||||
echo "count: $count"
|
||||
|
||||
- name: "DEBUG trap - in loop"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
for i in 1 2; do
|
||||
echo "iter $i"
|
||||
done
|
||||
|
||||
#
|
||||
# EXIT trap - advanced cases
|
||||
#
|
||||
- name: "EXIT trap - has access to exit status"
|
||||
stdin: |
|
||||
trap 'echo "exiting with: $?"' EXIT
|
||||
exit 42
|
||||
|
||||
- name: "EXIT trap - runs even after errexit"
|
||||
stdin: |
|
||||
set -e
|
||||
trap 'echo "[exit trap ran]"' EXIT
|
||||
false
|
||||
echo "not reached"
|
||||
|
||||
- name: "EXIT trap - can modify exit status"
|
||||
known_failure: true # TODO(traps): EXIT trap exit doesn't modify status
|
||||
stdin: |
|
||||
trap 'exit 0' EXIT
|
||||
exit 99
|
||||
|
||||
- name: "EXIT trap - multiple assignments last one wins"
|
||||
stdin: |
|
||||
trap 'echo "first"' EXIT
|
||||
trap 'echo "second"' EXIT
|
||||
echo "done"
|
||||
|
||||
#
|
||||
# EXIT trap in -c mode
|
||||
#
|
||||
- name: "EXIT trap fires in -c mode"
|
||||
args:
|
||||
- "-c"
|
||||
- "trap 'echo exiting' EXIT; echo hello"
|
||||
expected_stdout: |
|
||||
hello
|
||||
exiting
|
||||
|
||||
- name: "EXIT trap in -c mode has access to exit status"
|
||||
args:
|
||||
- "-c"
|
||||
- "trap 'echo \"status: $?\"' EXIT; exit 42"
|
||||
expected_stdout: "status: 42\n"
|
||||
|
||||
- name: "EXIT trap in -c mode runs after errexit"
|
||||
args:
|
||||
- "-c"
|
||||
- "set -e; trap 'echo trapped' EXIT; false"
|
||||
expected_stdout: "trapped\n"
|
||||
|
||||
- name: "EXIT trap in -c mode with multiple assignments last one wins"
|
||||
args:
|
||||
- "-c"
|
||||
- "trap 'echo first' EXIT; trap 'echo second' EXIT; echo done"
|
||||
expected_stdout: |
|
||||
done
|
||||
second
|
||||
|
||||
#
|
||||
# Trap with multiple signals
|
||||
#
|
||||
- name: "trap - clear multiple signals at once"
|
||||
stdin: |
|
||||
trap 'echo "[handler]"' INT TERM USR1
|
||||
trap -p INT
|
||||
trap -p TERM
|
||||
trap -p USR1
|
||||
echo "---"
|
||||
trap - INT TERM USR1
|
||||
trap -p INT
|
||||
trap -p TERM
|
||||
trap -p USR1
|
||||
echo "done"
|
||||
|
||||
- name: "trap - ignore multiple signals"
|
||||
stdin: |
|
||||
trap '' INT TERM
|
||||
trap -p INT
|
||||
trap -p TERM
|
||||
echo "done"
|
||||
@@ -0,0 +1,7 @@
|
||||
name: "true and false"
|
||||
cases:
|
||||
- name: "true and false ignore all args"
|
||||
stdin: |
|
||||
builtin false --help; echo "builtin false --help => $?"
|
||||
builtin true --help; echo "builtin true --help => $?"
|
||||
builtin true s87tien5; echo "builtin true s87tien5 => $?"
|
||||
@@ -0,0 +1,86 @@
|
||||
name: "Builtins: type"
|
||||
cases:
|
||||
- name: "Test type with no arguments"
|
||||
stdin: type
|
||||
|
||||
- name: "Test type with a valid command"
|
||||
stdin: type ls
|
||||
|
||||
- name: "Test type with an invalid command"
|
||||
ignore_stderr: true
|
||||
stdin: type invalid_command
|
||||
|
||||
- name: "Test type with -t option and a builtin command"
|
||||
stdin: type -t cd
|
||||
|
||||
- name: "Test type with -t option and an external command"
|
||||
stdin: type -t ls
|
||||
|
||||
- name: "Test type with -t option and non-existent command"
|
||||
ignore_stderr: true
|
||||
stdin: type -t non-existent
|
||||
|
||||
- name: "Test type with -a option and non-existent command"
|
||||
ignore_stderr: true
|
||||
stdin: type -a non-existent
|
||||
|
||||
- name: "Test type with -a option and a command with multiple definitions"
|
||||
stdin: type -a true
|
||||
|
||||
- name: Test type with -p option and a builtin command
|
||||
stdin: type -p cd
|
||||
|
||||
- name: Test type with -p option and an external command
|
||||
stdin: type -p ls
|
||||
|
||||
- name: Test type with -p option and non-existent command
|
||||
stdin: type -p non-existent
|
||||
|
||||
- name: Test type with -P option and a builtin command
|
||||
stdin: type -P cd
|
||||
|
||||
- name: Test type with -P option and an external command
|
||||
stdin: type -P ls
|
||||
|
||||
- name: Test type with -P option and non-existent command
|
||||
stdin: type -P non-existent
|
||||
|
||||
- name: Test type with -f option and a function
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
function myfunc() { echo "Hello, world!"; }
|
||||
type -f myfunc
|
||||
|
||||
- name: Test type with -f option and a command
|
||||
stdin: type -f ls
|
||||
|
||||
- name: Test type with -a option and a function
|
||||
stdin: |
|
||||
function myfunc() { echo "Hello, world!"; }
|
||||
type -a myfunc
|
||||
|
||||
- name: Test type with hashed path
|
||||
stdin: |
|
||||
hash -p /some/ls ls
|
||||
type ls
|
||||
|
||||
- name: Test type -a with hashed path
|
||||
stdin: |
|
||||
hash -p /some/ls ls
|
||||
type -a ls
|
||||
|
||||
- name: Test type -P with hashed path
|
||||
stdin: |
|
||||
hash -p /some/ls ls
|
||||
type -P ls
|
||||
|
||||
- name: Test type -P -a with hashed path
|
||||
min_oracle_version: 5.3 # Behavior changed in bash 5.3
|
||||
stdin: |
|
||||
hash -p /some/ls ls
|
||||
type -P -a ls
|
||||
|
||||
- name: Test type -p -a with hashed path
|
||||
stdin: |
|
||||
hash -p /some/ls ls
|
||||
type -p -a ls
|
||||
@@ -0,0 +1,10 @@
|
||||
name: "Builtins: typeset"
|
||||
|
||||
cases:
|
||||
- name: "Display vars"
|
||||
stdin: |
|
||||
typeset myvar=something
|
||||
typeset -p myvar
|
||||
|
||||
typeset myarr=(a b c)
|
||||
typeset -p myarr
|
||||
@@ -0,0 +1,37 @@
|
||||
name: "Builtins: ulimit"
|
||||
cases:
|
||||
- name: "ulimit -c"
|
||||
stdin: |
|
||||
ulimit -c
|
||||
|
||||
- name: "ulimit -d"
|
||||
stdin: |
|
||||
ulimit -d
|
||||
|
||||
- name: "ulimit -f"
|
||||
stdin: |
|
||||
ulimit -f
|
||||
|
||||
- name: "ulimit -l"
|
||||
stdin: |
|
||||
ulimit -l
|
||||
|
||||
- name: "ulimit -m"
|
||||
stdin: |
|
||||
ulimit -m
|
||||
|
||||
- name: "ulimit -n"
|
||||
stdin: |
|
||||
ulimit -n
|
||||
|
||||
- name: "ulimit -d unlimited"
|
||||
stdin: |
|
||||
ulimit -d unlimited
|
||||
|
||||
- name: "ulimit -f unlimited"
|
||||
stdin: |
|
||||
ulimit -f unlimited
|
||||
|
||||
- name: "ulimit -m unlimited"
|
||||
stdin: |
|
||||
ulimit -m unlimited
|
||||
@@ -0,0 +1,15 @@
|
||||
name: "Builtins: unalias"
|
||||
cases:
|
||||
- name: "Unalias basic usage"
|
||||
stdin: |
|
||||
shopt -s expand_aliases
|
||||
alias echo='echo prefixed'
|
||||
echo 'something'
|
||||
unalias echo
|
||||
echo 'something'
|
||||
|
||||
- name: "Unalias non-existent alias"
|
||||
ignore_stderr: true # Slightly different error messages
|
||||
stdin: |
|
||||
shopt -s expand_aliases
|
||||
unalias not_an_alias
|
||||
@@ -0,0 +1,261 @@
|
||||
name: "Builtins: unset"
|
||||
cases:
|
||||
- name: "Basic unset usage"
|
||||
stdin: |
|
||||
var=value
|
||||
unset var
|
||||
echo "var: ${var}"
|
||||
|
||||
- name: "Unset multiple variables"
|
||||
stdin: |
|
||||
var1=value1
|
||||
var2=value2
|
||||
unset var1 var2
|
||||
echo "var1: ${var1}, var2: ${var2}"
|
||||
|
||||
- name: "Unset function"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
function myfunc() {
|
||||
echo "Hello, world!"
|
||||
}
|
||||
unset myfunc
|
||||
myfunc
|
||||
|
||||
- name: "Unset -f function"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
function myfunc() {
|
||||
echo "Hello, world!"
|
||||
}
|
||||
unset -f myfunc
|
||||
myfunc
|
||||
|
||||
- name: "Unset indexed array element"
|
||||
stdin: |
|
||||
declare -a myarray=(a b c)
|
||||
|
||||
unset myarray[0]
|
||||
declare -p myarray
|
||||
|
||||
unset myarray[3]
|
||||
declare -p myarray
|
||||
|
||||
unset myarray[2]
|
||||
declare -p myarray
|
||||
|
||||
unset myarray[1]
|
||||
declare -p myarray
|
||||
|
||||
- name: "Unset associative array element"
|
||||
stdin: |
|
||||
echo "[1]"
|
||||
|
||||
declare -A myarray=([key]=value)
|
||||
|
||||
unset myarray[key]
|
||||
declare -p myarray
|
||||
|
||||
echo "[2]"
|
||||
|
||||
declare -A myarray=([key]=value)
|
||||
|
||||
unset "myarray[key]"
|
||||
declare -p myarray
|
||||
|
||||
echo "[3]"
|
||||
|
||||
declare -A myarray=([key]=value)
|
||||
key="x:y:z"
|
||||
|
||||
unset "myarray[key]"
|
||||
declare -p myarray
|
||||
|
||||
- name: "Unset array element with interesting expression"
|
||||
stdin: |
|
||||
declare -a myarray=(a b c d e)
|
||||
|
||||
echo "Initial array:"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
i=4
|
||||
unset myarray[i]
|
||||
echo "After removing element at index 4:"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
unset myarray[6/2]
|
||||
echo "After removing element at index 3:"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
- name: "Unset array element with negative index"
|
||||
stdin: |
|
||||
declare -a myarray=(a b c d e)
|
||||
|
||||
echo "Initial array:"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
unset "myarray[-1]"
|
||||
echo "After removing last element (index -1):"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
unset "myarray[-1]"
|
||||
echo "After removing new last element (index -1 again):"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
unset "myarray[-2]"
|
||||
echo "After removing second-to-last element (index -2):"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
- name: "Unset array element with negative index out of bounds"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
declare -a myarray=(a b c)
|
||||
|
||||
echo "Initial array:"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
unset "myarray[-4]"
|
||||
echo "After attempting to remove with index -4 (out of bounds):"
|
||||
declare -p myarray
|
||||
echo "Return code: $?"
|
||||
|
||||
- name: "Unset array element with variable negative index"
|
||||
stdin: |
|
||||
declare -a myarray=(10 20 30 40 50)
|
||||
|
||||
echo "Initial array:"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
i=-1
|
||||
unset "myarray[i]"
|
||||
echo "After removing with variable index i=-1:"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
i=-2
|
||||
unset "myarray[i]"
|
||||
echo "After removing with variable index i=-2:"
|
||||
declare -p myarray
|
||||
echo
|
||||
|
||||
- name: "Unset local in same function"
|
||||
stdin: |
|
||||
var="global"
|
||||
|
||||
myfunc() {
|
||||
echo "In myfunc"
|
||||
local -i var="10"
|
||||
declare -p var
|
||||
unset var
|
||||
echo "After unset"
|
||||
declare -p var
|
||||
var="20"
|
||||
}
|
||||
|
||||
echo "Before call: var=${var}"
|
||||
myfunc
|
||||
echo "After call: var=${var}"
|
||||
|
||||
- name: "Unset locals in callers"
|
||||
stdin: |
|
||||
firstfunc() {
|
||||
local var="first"
|
||||
echo "entered firstfunc: var=${var}"
|
||||
secondfunc
|
||||
echo "leaving firstfunc: var=${var}"
|
||||
}
|
||||
|
||||
secondfunc() {
|
||||
local var="second"
|
||||
echo "entered secondfunc: var=${var}"
|
||||
thirdfunc
|
||||
echo "leaving secondfunc: var=${var}"
|
||||
}
|
||||
|
||||
thirdfunc() {
|
||||
echo "entered thirdfunc: var=${var}"
|
||||
unset -v var
|
||||
echo "after first unset: var=${var}"
|
||||
var+=":updated"
|
||||
echo " ...and updated: var=${var}"
|
||||
unset -v var
|
||||
echo "after second unset: var=${var}"
|
||||
var+=":updated"
|
||||
echo " ...and updated: var=${var}"
|
||||
unset -v var
|
||||
echo "after third unset: var=${var}"
|
||||
var+=":updated"
|
||||
echo " ...and updated: var=${var}"
|
||||
echo "leaving firstfunc: var=${var}"
|
||||
}
|
||||
|
||||
var="global"
|
||||
echo "before calls: var=${var}"
|
||||
firstfunc
|
||||
echo "after calls: var=${var}"
|
||||
|
||||
- name: "Unset with nameref"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
declare -n ref=var
|
||||
var="value"
|
||||
|
||||
echo "[Before unset]"
|
||||
echo "ref: ${ref}"
|
||||
echo "var: ${var}"
|
||||
|
||||
unset ref
|
||||
|
||||
echo "[After unset]"
|
||||
echo "ref: ${ref}"
|
||||
echo "var: ${var}"
|
||||
|
||||
- name: "Unset -n removes nameref itself"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
target="value"
|
||||
declare -n ref=target
|
||||
echo "before: target=$target ref=$ref"
|
||||
unset -n ref
|
||||
echo "after unset -n: target=$target"
|
||||
declare -p ref 2>/dev/null || echo "ref is gone"
|
||||
declare -p target 2>/dev/null && echo "target still declared"
|
||||
|
||||
- name: "Unset array element through nameref"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
target=(a b c d e)
|
||||
declare -n ref=target
|
||||
unset 'ref[2]'
|
||||
declare -p target
|
||||
unset 'ref[0]'
|
||||
declare -p target
|
||||
|
||||
- name: "Unset odd function names"
|
||||
stdin: |
|
||||
echo "[Checking existing]"
|
||||
type [
|
||||
|
||||
echo "[Defining function]"
|
||||
function [() {
|
||||
:
|
||||
}
|
||||
|
||||
echo "[Checking defined]"
|
||||
type [
|
||||
|
||||
echo "[Unsetting function]"
|
||||
unset [
|
||||
echo "unset result: $?"
|
||||
|
||||
echo "[Checking after unset]"
|
||||
type [
|
||||
@@ -0,0 +1,52 @@
|
||||
name: "Builtins: wait"
|
||||
cases:
|
||||
- name: "wait for specific failed PID"
|
||||
known_failure: true # TODO(jobs): wait should return job's exit status
|
||||
stdin: |
|
||||
(exit 5) &
|
||||
pid=$!
|
||||
wait $pid
|
||||
echo "status: $?"
|
||||
|
||||
- name: "wait for successful PID"
|
||||
stdin: |
|
||||
(exit 0) &
|
||||
pid=$!
|
||||
wait $pid
|
||||
echo "status: $?"
|
||||
|
||||
- name: "wait in conditional for failed PID"
|
||||
known_failure: true # TODO(jobs): wait should return job's exit status
|
||||
stdin: |
|
||||
(exit 5) &
|
||||
pid=$!
|
||||
if wait $pid; then
|
||||
echo "then"
|
||||
else
|
||||
echo "else"
|
||||
fi
|
||||
|
||||
- name: "wait with no arguments"
|
||||
stdin: |
|
||||
(exit 0) &
|
||||
(exit 0) &
|
||||
wait
|
||||
echo "after wait all"
|
||||
|
||||
- name: "wait -n not implemented"
|
||||
known_failure: true # TODO(builtins): wait -n not implemented
|
||||
stdin: |
|
||||
(exit 0) &
|
||||
(exit 5) &
|
||||
wait -n
|
||||
echo "first done, status: $?"
|
||||
wait -n
|
||||
echo "second done, status: $?"
|
||||
|
||||
- name: "Background job failure with wait"
|
||||
known_failure: true # TODO(jobs): wait should return job's exit status
|
||||
stdin: |
|
||||
{ sleep 0.01; exit 3; } &
|
||||
pid=$!
|
||||
wait $pid
|
||||
echo "status: $?"
|
||||
@@ -0,0 +1,637 @@
|
||||
name: "Call stack"
|
||||
common_test_files:
|
||||
- path: "callstack_helpers.sh"
|
||||
contents: |
|
||||
# Helper to dump frame-independent call stack state.
|
||||
# Optionally takes a prefix string to use in all output.
|
||||
dump_source_info() {
|
||||
local prefix="${1:-}"
|
||||
|
||||
# Normalize shell name to make sure it matches.
|
||||
# TODO: Once all tests are run on 5.3, this will need to be revisited.
|
||||
echo "${prefix} caller: $(caller 2>&1 || echo '<failed>')" | sed -e "s|$0|main|g" | sed -e "s/environment/main/g"
|
||||
|
||||
echo "${prefix} LINENO: ${LINENO}"
|
||||
echo "${prefix} FUNCNAME[@]: ${FUNCNAME[*]}"
|
||||
|
||||
# Normalize shell name to make sure it matches.
|
||||
# TODO: Once all tests are run on 5.3, this will need to be revisited.
|
||||
echo "${prefix} BASH_SOURCE[@]: ${BASH_SOURCE[*]}" | sed -e "s|$0|main|g" | sed -e "s/environment/main/g"
|
||||
|
||||
# TODO(BASH_LINENO): implement correct BASH_LINENO tracking
|
||||
# echo "${prefix} BASH_LINENO[@]: ${BASH_LINENO[*]}"
|
||||
}
|
||||
|
||||
# Helper to dump frame-specific call stack state
|
||||
dump_frame() {
|
||||
local prefix="${1:-}"
|
||||
local frame="${2:-0}"
|
||||
|
||||
echo "${prefix}[Frame $frame]"
|
||||
|
||||
# Normalize shell name to make sure it matches.
|
||||
# TODO: Once all tests are run on 5.3, this will need to be revisited.
|
||||
echo "${prefix} caller N: $(caller $frame 2>&1 || echo '<failed>')" | sed -e "s|$0|main|g" | sed -e "s/environment/main/g"
|
||||
}
|
||||
|
||||
# Helper to dump all stack info
|
||||
dump_stack() {
|
||||
local prefix="${1:-}"
|
||||
|
||||
dump_source_info "$prefix"
|
||||
|
||||
local max_frames="${2:-20}"
|
||||
local i
|
||||
for ((i=0; i<max_frames; i++)); do
|
||||
if ! caller $i >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
dump_frame "$prefix" $i
|
||||
done
|
||||
}
|
||||
|
||||
cases:
|
||||
- name: "No calls - top level"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
echo "=== Top level ==="
|
||||
dump_stack
|
||||
|
||||
- name: "Simple function call"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_function() {
|
||||
echo "=== In my_function ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
my_function
|
||||
|
||||
- name: "Nested function calls"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
level3() {
|
||||
echo "=== In level3 ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
level2() {
|
||||
echo "=== In level2 ==="
|
||||
dump_stack " "
|
||||
level3
|
||||
}
|
||||
|
||||
level1() {
|
||||
echo "=== In level1 ==="
|
||||
dump_stack " "
|
||||
level2
|
||||
}
|
||||
|
||||
level1
|
||||
|
||||
- name: "Function call from sourced script"
|
||||
test_files:
|
||||
- path: "sourced.sh"
|
||||
contents: |
|
||||
sourced_function() {
|
||||
echo "=== In sourced_function ==="
|
||||
dump_stack " "
|
||||
}
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
source sourced.sh
|
||||
sourced_function
|
||||
|
||||
- name: "Nested sourcing"
|
||||
test_files:
|
||||
- path: "outer.sh"
|
||||
contents: |
|
||||
echo "=== In outer.sh top level ==="
|
||||
dump_stack " "
|
||||
source inner.sh
|
||||
- path: "inner.sh"
|
||||
contents: |
|
||||
echo "=== In inner.sh top level ==="
|
||||
dump_stack " "
|
||||
|
||||
inner_func() {
|
||||
echo "=== In inner_func ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
inner_func
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
command source outer.sh
|
||||
|
||||
- name: "Command substitution in top level"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
result=$(dump_stack " [CmdSub] ")
|
||||
echo "$result"
|
||||
|
||||
- name: "Command substitution in function"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_function() {
|
||||
echo "=== In my_function ==="
|
||||
result=$(dump_stack " [CmdSub] ")
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
my_function
|
||||
|
||||
- name: "Command substitution invoking function"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_function() {
|
||||
echo "=== In my_function via cmdSub ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
result=$(my_function)
|
||||
echo "$result"
|
||||
|
||||
- name: "Nested command substitutions"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
inner_func() {
|
||||
echo "=== In inner_func ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
outer_func() {
|
||||
echo "=== In outer_func ==="
|
||||
result=$(inner_func)
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
final=$(outer_func)
|
||||
echo "$final"
|
||||
|
||||
- name: "eval at top level"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
eval 'echo "=== In eval ==="; dump_stack " "'
|
||||
|
||||
- name: "eval in function"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_function() {
|
||||
echo "=== In my_function ==="
|
||||
eval 'echo "=== In eval ==="; dump_stack " "'
|
||||
}
|
||||
|
||||
my_function
|
||||
|
||||
- name: "eval calling function"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_function() {
|
||||
echo "=== In my_function via eval ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
eval 'my_function'
|
||||
|
||||
- name: "Nested evals"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_function() {
|
||||
echo "=== In my_function ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
eval 'eval "my_function"'
|
||||
|
||||
- name: "eval with command substitution"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_function() {
|
||||
echo "=== In my_function ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
result=$(eval 'my_function')
|
||||
echo "$result"
|
||||
|
||||
- name: "Trap handler - DEBUG"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
trap_handler() {
|
||||
echo "=== In DEBUG trap handler ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
trap 'trap_handler' DEBUG
|
||||
|
||||
echo "Command 1"
|
||||
echo "Command 2"
|
||||
|
||||
- name: "Trap handler - DEBUG in function"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
trap_handler() {
|
||||
echo "=== In DEBUG trap handler ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
trap 'trap_handler' DEBUG
|
||||
|
||||
my_function() {
|
||||
echo "In my_function"
|
||||
echo "Still in my_function"
|
||||
}
|
||||
|
||||
my_function
|
||||
|
||||
- name: "Trap handler - EXIT"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
trap 'echo "=== In EXIT trap ==="; dump_stack " "' EXIT
|
||||
|
||||
echo "Main script"
|
||||
|
||||
- name: "Trap handler calling function"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
trap_function() {
|
||||
echo "=== In trap_function ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
trap 'trap_function' EXIT
|
||||
|
||||
echo "Main script"
|
||||
|
||||
- name: "LINENO tracking across contexts"
|
||||
stdin: |
|
||||
echo "Line 1: LINENO=${LINENO}"
|
||||
echo "Line 2: LINENO=${LINENO}"
|
||||
|
||||
my_function() {
|
||||
echo "In function line 1: LINENO=${LINENO}"
|
||||
echo "In function line 2: LINENO=${LINENO}"
|
||||
}
|
||||
|
||||
my_function
|
||||
|
||||
echo "Line 3: LINENO=${LINENO}"
|
||||
|
||||
- name: "LINENO in eval"
|
||||
stdin: |
|
||||
echo "Before eval: LINENO=${LINENO}"
|
||||
eval 'echo "In eval: LINENO=${LINENO}"'
|
||||
echo "After eval: LINENO=${LINENO}"
|
||||
|
||||
- name: "LINENO in command substitution"
|
||||
stdin: |
|
||||
echo "Before cmdSub: LINENO=${LINENO}"
|
||||
result=$(echo "In cmdSub: LINENO=${LINENO}")
|
||||
echo "$result"
|
||||
echo "After cmdSub: LINENO=${LINENO}"
|
||||
|
||||
- name: "LINENO in sourced script"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "Sourced line 1: LINENO=${LINENO}"
|
||||
echo "Sourced line 2: LINENO=${LINENO}"
|
||||
|
||||
sourced_func() {
|
||||
echo "In sourced_func: LINENO=${LINENO}"
|
||||
}
|
||||
|
||||
sourced_func
|
||||
stdin: |
|
||||
echo "Main line 1: LINENO=${LINENO}"
|
||||
source script.sh
|
||||
echo "Main line 2: LINENO=${LINENO}"
|
||||
|
||||
- name: "different depths"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
level3() {
|
||||
dump_stack
|
||||
}
|
||||
|
||||
level2() {
|
||||
level3
|
||||
}
|
||||
|
||||
level1() {
|
||||
level2
|
||||
}
|
||||
|
||||
level1
|
||||
|
||||
- name: "BASH_SOURCE and FUNCNAME arrays"
|
||||
test_files:
|
||||
- path: "outer.sh"
|
||||
contents: |
|
||||
outer_func() {
|
||||
echo "=== In outer_func ==="
|
||||
echo " FUNCNAME[@]: ${FUNCNAME[*]}"
|
||||
echo " BASH_SOURCE[@]: ${BASH_SOURCE[*]}"
|
||||
source inner.sh
|
||||
}
|
||||
- path: "inner.sh"
|
||||
contents: |
|
||||
inner_func() {
|
||||
echo "=== In inner_func ==="
|
||||
echo " FUNCNAME[@]: ${FUNCNAME[*]}"
|
||||
echo " BASH_SOURCE[@]: ${BASH_SOURCE[*]}"
|
||||
}
|
||||
|
||||
inner_func
|
||||
stdin: |
|
||||
source outer.sh
|
||||
outer_func
|
||||
|
||||
- name: "Mixed: eval in sourced function with command substitution"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
script_func() {
|
||||
echo "=== In script_func ==="
|
||||
result=$(eval 'dump_stack " [Eval->CmdSub] "')
|
||||
echo "$result"
|
||||
}
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
source script.sh
|
||||
script_func
|
||||
|
||||
- name: "Complex nesting: function -> eval -> cmdSub -> function"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
inner_func() {
|
||||
echo "=== In inner_func ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
outer_func() {
|
||||
echo "=== In outer_func ==="
|
||||
eval 'result=$(inner_func); echo "$result"'
|
||||
}
|
||||
|
||||
outer_func
|
||||
|
||||
# Tests with -c flag
|
||||
- name: "Call stack with -c flag"
|
||||
args:
|
||||
["-c", "source callstack_helpers.sh; echo '=== Via -c ==='; dump_stack"]
|
||||
|
||||
- name: "Function call with -c flag"
|
||||
args:
|
||||
[
|
||||
"-c",
|
||||
"my_func() { echo '=== In my_func via -c ==='; dump_stack; }; source callstack_helpers.sh; my_func",
|
||||
]
|
||||
|
||||
- name: "Nested functions with -c flag"
|
||||
args:
|
||||
[
|
||||
"-c",
|
||||
"f2() { echo 'In f2'; dump_stack; }; f1() { echo 'In f1'; f2; }; source callstack_helpers.sh; f1",
|
||||
]
|
||||
|
||||
- name: "eval with -c flag"
|
||||
args:
|
||||
[
|
||||
"-c",
|
||||
"eval 'source callstack_helpers.sh; echo In eval via -c; dump_stack'",
|
||||
]
|
||||
|
||||
- name: "Command substitution with -c flag"
|
||||
args:
|
||||
[
|
||||
"-c",
|
||||
"source callstack_helpers.sh; result=$(echo LINENO=${LINENO}; dump_stack); echo $result",
|
||||
]
|
||||
|
||||
# Interactive mode tests
|
||||
- name: "Interactive mode - simple function"
|
||||
known_failure: true # TODO(LINENO): implement accurate interactive mode tracking
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
f() { dump_stack; }
|
||||
f
|
||||
|
||||
- name: "Interactive mode - FUNCNAME and BASH_SOURCE"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
f() { echo "FUNCNAME: ${FUNCNAME[@]}"; echo "BASH_SOURCE: ${BASH_SOURCE[@]}"; }
|
||||
f
|
||||
|
||||
# PROMPT_COMMAND tests
|
||||
- name: "PROMPT_COMMAND with call stack"
|
||||
known_failure: true # TODO(LINENO): implement accurate PROMPT_COMMAND line tracking
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
PROMPT_COMMAND='dump_stack "[PC] "'
|
||||
echo test
|
||||
|
||||
- name: "PROMPT_COMMAND calling function"
|
||||
known_failure: true # TODO(LINENO): implement accurate PROMPT_COMMAND line tracking
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
pc_func() { echo "[PC_FUNC] In function"; dump_stack; }
|
||||
PROMPT_COMMAND='pc_func'
|
||||
echo test
|
||||
|
||||
- name: "PROMPT_COMMAND with nested calls"
|
||||
known_failure: true # TODO(LINENO): implement accurate PROMPT_COMMAND line tracking
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
inner() { dump_stack; }
|
||||
outer() { inner; }
|
||||
PROMPT_COMMAND='outer'
|
||||
echo test
|
||||
|
||||
# Additional edge cases
|
||||
- name: "Recursive function call stack"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
recursive_func() {
|
||||
local depth=$1
|
||||
echo "=== Depth $depth ==="
|
||||
|
||||
if [ $depth -gt 0 ]; then
|
||||
recursive_func $((depth - 1))
|
||||
else
|
||||
dump_stack " "
|
||||
fi
|
||||
}
|
||||
|
||||
recursive_func 3
|
||||
|
||||
- name: "Call stack with process substitution"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_func() {
|
||||
echo "=== In my_func ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
# Process substitution context
|
||||
cat <(my_func)
|
||||
|
||||
- name: "Call stack across pipe"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_func() {
|
||||
echo "=== In my_func ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
my_func | cat
|
||||
|
||||
- name: "Call stack in subshell"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_func() {
|
||||
echo "=== In my_func (subshell) ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
(my_func)
|
||||
|
||||
- name: "Call stack with background job"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_func() {
|
||||
echo "=== In my_func (background) ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
my_func &
|
||||
wait
|
||||
|
||||
- name: "BASH_SOURCE indexing"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "In script.sh"
|
||||
echo " BASH_SOURCE[0]: ${BASH_SOURCE[0]}"
|
||||
echo " BASH_SOURCE[1]: ${BASH_SOURCE[1]}"
|
||||
|
||||
script_func() {
|
||||
echo "In script_func"
|
||||
echo " BASH_SOURCE[0]: ${BASH_SOURCE[0]}"
|
||||
echo " BASH_SOURCE[1]: ${BASH_SOURCE[1]}"
|
||||
echo " BASH_SOURCE[2]: ${BASH_SOURCE[2]}"
|
||||
}
|
||||
|
||||
script_func
|
||||
stdin: |
|
||||
echo "Main script"
|
||||
echo " BASH_SOURCE[0]: ${BASH_SOURCE[0]}"
|
||||
source script.sh
|
||||
|
||||
- name: "FUNCNAME at top level"
|
||||
stdin: |
|
||||
echo "Top level FUNCNAME: '${FUNCNAME}'"
|
||||
echo "Top level FUNCNAME[@]: '${FUNCNAME[@]}'"
|
||||
echo "Top level FUNCNAME[*]: '${FUNCNAME[*]}'"
|
||||
|
||||
- name: "Empty BASH_LINENO at top level"
|
||||
stdin: |
|
||||
echo "Top level BASH_LINENO[@]: '${BASH_LINENO[@]}'"
|
||||
echo "Top level BASH_LINENO[*]: '${BASH_LINENO[*]}'"
|
||||
|
||||
- name: "Call stack in trap with function call"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
trap_func() {
|
||||
echo "=== In trap_func ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
my_func() {
|
||||
echo "=== In my_func ==="
|
||||
echo "About to trigger trap"
|
||||
}
|
||||
|
||||
trap 'trap_func' DEBUG
|
||||
my_func
|
||||
|
||||
- name: "Call stack: source within eval within function"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
echo "=== In sourced script ==="
|
||||
dump_stack " "
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_func() {
|
||||
echo "=== In my_func ==="
|
||||
eval 'source script.sh'
|
||||
}
|
||||
|
||||
my_func
|
||||
|
||||
- name: "Multiple eval levels"
|
||||
stdin: |
|
||||
source callstack_helpers.sh
|
||||
|
||||
my_func() {
|
||||
echo "=== In my_func ==="
|
||||
dump_stack " "
|
||||
}
|
||||
|
||||
eval 'eval "eval \"my_func\""'
|
||||
|
||||
- name: "LINENO persistence in function"
|
||||
stdin: |
|
||||
my_func() {
|
||||
local line1=${LINENO}
|
||||
local line2=${LINENO}
|
||||
local line3=${LINENO}
|
||||
echo "line1: $line1"
|
||||
echo "line2: $line2"
|
||||
echo "line3: $line3"
|
||||
}
|
||||
|
||||
my_func
|
||||
@@ -0,0 +1,9 @@
|
||||
name: "Complete commands"
|
||||
cases:
|
||||
- name: "Multi-command sequence"
|
||||
stdin: |
|
||||
echo 1; echo 2; echo 3
|
||||
|
||||
- name: "Semicolon-terminated sequence"
|
||||
stdin: |
|
||||
echo 1; echo 2;
|
||||
@@ -0,0 +1,16 @@
|
||||
name: "Compound commands: arithmetic"
|
||||
cases:
|
||||
- name: "Basic arithmetic statements"
|
||||
stdin: |
|
||||
((0 == 0)) && echo "0 == 0"
|
||||
((0 != 0)) && echo "0 != 0"
|
||||
|
||||
- name: "Arithmetic statements with parens"
|
||||
stdin: |
|
||||
(( (0) )) && echo "0"
|
||||
(( (1) )) && echo "1"
|
||||
|
||||
- name: "Arithmetic statements with parens and operators"
|
||||
stdin: |
|
||||
(( (0) == 0 )) && echo "0 == 0"
|
||||
(( (1) != 0 )) && echo "1 != 0"
|
||||
@@ -0,0 +1,110 @@
|
||||
name: "Compound commands: arithmetic for"
|
||||
cases:
|
||||
- name: "Single-line arithmetic for loop"
|
||||
stdin: |
|
||||
for ((i = 0; i < 5; i++)); do echo $i; done
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Arithmetic for loop without separator"
|
||||
stdin: |
|
||||
for ((i = 0; i < 5; i++)) do echo $i; done
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Arithmetic for loop with empty condition"
|
||||
stdin: |
|
||||
for (( ; ; )); do
|
||||
echo "In loop; status: $?"
|
||||
break
|
||||
done
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Arithmetic for loop with ;;"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
for ((;;)); do
|
||||
echo "In loop; status: $?"
|
||||
break
|
||||
done
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Break in arithmetic for loop"
|
||||
stdin: |
|
||||
for ((i = 0; i < 5; i++)); do
|
||||
echo $i
|
||||
break
|
||||
done
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Continue in arithmetic for loop"
|
||||
stdin: |
|
||||
for ((i = 0; i < 5; i++)); do
|
||||
continue
|
||||
echo "Should not print"
|
||||
done
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "Arithmetic for with break in condition inside outer loop"
|
||||
stdin: |
|
||||
for j in 1 2 3; do
|
||||
echo "Outer iteration $j"
|
||||
for ((i = 0; break; i++)); do
|
||||
echo "Inner body"
|
||||
done
|
||||
echo "After inner for"
|
||||
done
|
||||
echo "After outer loop"
|
||||
|
||||
- name: "Arithmetic for with continue in condition inside outer loop"
|
||||
stdin: |
|
||||
for j in 1 2 3; do
|
||||
echo "Outer iteration $j"
|
||||
for ((i = 0; continue; i++)); do
|
||||
echo "Inner body"
|
||||
done
|
||||
echo "After inner for"
|
||||
done
|
||||
echo "After outer loop"
|
||||
|
||||
- name: "Arithmetic for with return in body"
|
||||
stdin: |
|
||||
f() {
|
||||
for ((i = 0; i < 5; i++)); do
|
||||
echo "Iteration $i"
|
||||
if [ $i -eq 2 ]; then
|
||||
return 42
|
||||
fi
|
||||
done
|
||||
echo "After for"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "Arithmetic for with exit in body"
|
||||
stdin: |
|
||||
for ((i = 0; i < 5; i++)); do
|
||||
echo "Iteration $i"
|
||||
if [ $i -eq 2 ]; then
|
||||
exit 42
|
||||
fi
|
||||
done
|
||||
echo "After for"
|
||||
|
||||
- name: "Arithmetic for with alternate syntax"
|
||||
stdin: |
|
||||
for ((i = 0; i < 5; i++)) {
|
||||
echo "Iteration $i"
|
||||
}
|
||||
|
||||
- name: "Arithmetic for with alternate syntax (one line)"
|
||||
stdin: |
|
||||
for ((i = 0; i < 5; i++)) { echo "Iteration $i"; }
|
||||
|
||||
- name: "Arithmetic for loop with nameref"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
target=0
|
||||
declare -n ref=target
|
||||
for ((ref=1; ref<=3; ref++)); do
|
||||
echo "ref=$ref target=$target"
|
||||
done
|
||||
echo "final target=$target"
|
||||
@@ -0,0 +1,5 @@
|
||||
name: "Compound commands: brace"
|
||||
cases:
|
||||
- name: "Brace command"
|
||||
stdin: |
|
||||
{ echo 'hi'; }
|
||||
@@ -0,0 +1,185 @@
|
||||
name: "Compound commands: case"
|
||||
cases:
|
||||
- name: "Basic case statement with double semi"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
case x in
|
||||
x) echo hi;;
|
||||
esac
|
||||
args: ["./script.sh"]
|
||||
|
||||
- name: "Basic catch-all pattern against multi-line input"
|
||||
stdin: |
|
||||
case "$(echo hi; echo there)" in
|
||||
"not-it") echo "did not work" ;;
|
||||
*) echo "caught it" ;;
|
||||
esac
|
||||
|
||||
- name: "One-line case statement with double semi"
|
||||
stdin: |
|
||||
case x in x) echo "hi";; esac
|
||||
|
||||
- name: "Interesting characters in cases"
|
||||
stdin: |
|
||||
case "{" in
|
||||
{) echo "curly brace" ;;
|
||||
*) echo "unhandled case" ;;
|
||||
esac
|
||||
|
||||
- name: "Case with case insensitive pattern"
|
||||
stdin: |
|
||||
shopt -s nocasematch
|
||||
case "A" in
|
||||
a) echo "matched" ;;
|
||||
*) echo "did not match" ;;
|
||||
esac
|
||||
|
||||
- name: "Interesting patterns in cases"
|
||||
stdin: |
|
||||
for word in "-a" "!b" "*c" "(d" "{e" ":f" "'g"; do
|
||||
case "${word}" in
|
||||
\!*) echo "starts with exclamation" ;;
|
||||
-*) echo "starts with hyphen" ;;
|
||||
\**) echo "starts with asterisk" ;;
|
||||
\(*) echo "starts with open parenthesis" ;;
|
||||
\{*) echo "starts with open curly brace" ;;
|
||||
:*) echo "starts with colon" ;;
|
||||
\'*) echo "starts with single quote" ;;
|
||||
*) echo "unhandled case" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
- name: "Empty case"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
case abc in
|
||||
*b*) ;;
|
||||
*) return 33;;
|
||||
esac
|
||||
|
||||
echo "Dropped out"
|
||||
}
|
||||
|
||||
myfunc
|
||||
|
||||
- name: "Case with non-dsemi"
|
||||
stdin: |
|
||||
case "b" in
|
||||
a) echo "a";;
|
||||
b) echo "b"
|
||||
esac
|
||||
|
||||
- name: "Case with fall-through"
|
||||
stdin: |
|
||||
case "b" in
|
||||
a) echo "a";;
|
||||
b) echo "b";&
|
||||
c) echo "c";;
|
||||
d) echo "d";;
|
||||
esac
|
||||
|
||||
- name: "Case with resuming switch"
|
||||
stdin: |
|
||||
case "b" in
|
||||
a) echo "a";;
|
||||
b) echo "b";;&
|
||||
c) echo "c";;
|
||||
*) echo "*";;
|
||||
d) echo "d";;
|
||||
esac
|
||||
|
||||
- name: "Case status values"
|
||||
stdin: |
|
||||
function yield() {
|
||||
return $1
|
||||
}
|
||||
|
||||
case "a" in
|
||||
x) yield 10;;
|
||||
a) yield 11;;
|
||||
b) yield 12;;
|
||||
esac
|
||||
echo "1: $?"
|
||||
|
||||
case "a" in
|
||||
b) yield 10;;
|
||||
c) yield 11;;
|
||||
esac
|
||||
echo "2: $?"
|
||||
|
||||
case "a" in
|
||||
a) yield 10;&
|
||||
b) yield 11;;
|
||||
c) yield 12;;
|
||||
esac
|
||||
echo "3: $?"
|
||||
|
||||
case "a" in
|
||||
a) yield 10;;&
|
||||
*) yield 11;;
|
||||
x) yield 12;;
|
||||
esac
|
||||
echo "4: $?"
|
||||
|
||||
- name: "Case with reserved words"
|
||||
stdin: |
|
||||
case case in
|
||||
case) echo "saw case";;
|
||||
esac
|
||||
|
||||
- name: "Case with return in function"
|
||||
stdin: |
|
||||
f() {
|
||||
case "a" in
|
||||
a) echo "matched a"; return 42;;
|
||||
b) echo "matched b";;
|
||||
esac
|
||||
echo "After case"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "Case with exit"
|
||||
stdin: |
|
||||
case "a" in
|
||||
a) echo "matched a"; exit 42;;
|
||||
b) echo "matched b";;
|
||||
esac
|
||||
echo "After case"
|
||||
|
||||
- name: "Case with break in loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
case "$i" in
|
||||
1) echo "one";;
|
||||
2) echo "two"; break;;
|
||||
3) echo "three";;
|
||||
esac
|
||||
echo "After case iteration $i"
|
||||
done
|
||||
echo "After loop"
|
||||
|
||||
- name: "Case with continue in loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
case "$i" in
|
||||
1) echo "one";;
|
||||
2) echo "two"; continue;;
|
||||
3) echo "three";;
|
||||
esac
|
||||
echo "After case iteration $i"
|
||||
done
|
||||
echo "After loop"
|
||||
|
||||
- name: "Case with return and fall-through"
|
||||
stdin: |
|
||||
f() {
|
||||
case "a" in
|
||||
a) echo "matched a"; return 42;&
|
||||
b) echo "matched b";;
|
||||
esac
|
||||
echo "After case"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
@@ -0,0 +1,839 @@
|
||||
name: "Compound commands: coproc"
|
||||
|
||||
# NOTE: Coproc tests must defend against TWO race conditions caused by bash's
|
||||
# SIGCHLD handler:
|
||||
#
|
||||
# 1. The handler clears the COPROC array when the coproc child exits, so
|
||||
# ${COPROC[0]} can become empty before the parent captures it.
|
||||
# 2. The handler ALSO closes the underlying pipe FDs, so even after capturing
|
||||
# the integer FD, reading from it returns EOF immediately.
|
||||
#
|
||||
# We defend against both by:
|
||||
# (a) Having the coproc body block on `read -r _ < _sync` first, so it can't
|
||||
# exit before the parent dups the FD (no SIGCHLD can fire yet).
|
||||
# (b) Using `exec 9<&${COPROC[0]}` to dup the pipe to a fresh FD that bash
|
||||
# does NOT manage as part of the coproc, so it survives SIGCHLD cleanup.
|
||||
# (c) Then `echo x > _sync` releases the coproc to do its real work.
|
||||
#
|
||||
# We use explicit FD 9 (rather than `{fd}<&...` auto-assignment) for portability.
|
||||
|
||||
cases:
|
||||
#
|
||||
# Basic invocation
|
||||
#
|
||||
- name: "Unnamed coproc with brace group"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; echo "hello from coproc"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Unnamed coproc with subshell"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc ( read -r _ < _sync; echo "hello from subshell coproc" )
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Named coproc with brace group"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc MYCOPROC { read -r _ < _sync; echo "named coproc"; }
|
||||
exec 9<&${MYCOPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Named coproc with subshell"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc MYSUB ( read -r _ < _sync; echo "named subshell coproc" )
|
||||
exec 9<&${MYSUB[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc with simple command"
|
||||
skip: true # TODO(coproc): hangs - wait never completes after bidirectional coproc I/O
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc cat
|
||||
rfd=${COPROC[0]}
|
||||
wfd=${COPROC[1]}
|
||||
echo "input line" >&$wfd
|
||||
exec {wfd}>&-
|
||||
read -r line <&$rfd
|
||||
echo "got: $line"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc with multi-statement brace group"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
echo "line1"
|
||||
echo "line2"
|
||||
echo "line3"
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# COPROC and COPROC_PID variable validation via functional use
|
||||
#
|
||||
- name: "COPROC array read FD is functional"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; echo "via read fd"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
read -r line <&9
|
||||
echo "got: $line"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Named coproc FDs are functional"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc MYPROC { read -r _ < _sync; echo "named fd test"; }
|
||||
exec 9<&${MYPROC[0]}
|
||||
echo x > _sync
|
||||
read -r line <&9
|
||||
echo "got: $line"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Reading from coproc stdout
|
||||
#
|
||||
- name: "Read coproc output with cat"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; echo "output from coproc"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Read coproc output with read builtin"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; echo "readline test"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
read -r line <&9
|
||||
echo "got: $line"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Read multiple lines from coproc"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
echo "first"
|
||||
echo "second"
|
||||
echo "third"
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Read multiple lines with read builtin"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
echo "line1"
|
||||
echo "line2"
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
{
|
||||
read -r first
|
||||
read -r second
|
||||
} <&9
|
||||
echo "first=$first"
|
||||
echo "second=$second"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Named coproc read"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc READER { read -r _ < _sync; echo "from named"; }
|
||||
exec 9<&${READER[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Writing to coproc stdin
|
||||
#
|
||||
- name: "Write to coproc stdin and read back"
|
||||
skip: true # TODO(coproc): hangs - wait never completes after bidirectional coproc I/O
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc cat
|
||||
rfd=${COPROC[0]}
|
||||
wfd=${COPROC[1]}
|
||||
echo "hello coproc" >&$wfd
|
||||
exec {wfd}>&-
|
||||
read -r line <&$rfd
|
||||
echo "got: $line"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Bidirectional communication"
|
||||
skip: true # TODO(coproc): hangs - wait never completes after bidirectional coproc I/O
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc {
|
||||
while read -r line; do
|
||||
echo "processed: $line"
|
||||
done
|
||||
}
|
||||
rfd=${COPROC[0]}
|
||||
wfd=${COPROC[1]}
|
||||
echo "msg1" >&$wfd
|
||||
echo "msg2" >&$wfd
|
||||
exec {wfd}>&-
|
||||
{
|
||||
read -r out1
|
||||
read -r out2
|
||||
} <&$rfd
|
||||
echo "$out1"
|
||||
echo "$out2"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Named coproc bidirectional"
|
||||
skip: true # TODO(coproc): hangs - wait never completes after bidirectional coproc I/O
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc BIDIR {
|
||||
while read -r line; do
|
||||
echo "echo: $line"
|
||||
done
|
||||
}
|
||||
rfd=${BIDIR[0]}
|
||||
wfd=${BIDIR[1]}
|
||||
echo "test1" >&$wfd
|
||||
echo "test2" >&$wfd
|
||||
exec {wfd}>&-
|
||||
{
|
||||
read -r out1
|
||||
read -r out2
|
||||
} <&$rfd
|
||||
echo "$out1"
|
||||
echo "$out2"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Close write FD to signal EOF"
|
||||
skip: true # TODO(coproc): hangs - wait never completes after bidirectional coproc I/O
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc {
|
||||
count=0
|
||||
while read -r line; do
|
||||
count=$((count + 1))
|
||||
done
|
||||
echo "read $count lines"
|
||||
}
|
||||
rfd=${COPROC[0]}
|
||||
wfd=${COPROC[1]}
|
||||
echo "a" >&$wfd
|
||||
echo "b" >&$wfd
|
||||
echo "c" >&$wfd
|
||||
exec {wfd}>&-
|
||||
read -r result <&$rfd
|
||||
echo "$result"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Bidirectional with cat for EOF"
|
||||
skip: true # TODO(coproc): hangs - stdout pipe not fully closed after coproc exits
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc {
|
||||
while read -r line; do
|
||||
echo "processed: $line"
|
||||
done
|
||||
}
|
||||
rfd=${COPROC[0]}
|
||||
wfd=${COPROC[1]}
|
||||
echo "msg1" >&$wfd
|
||||
echo "msg2" >&$wfd
|
||||
exec {wfd}>&-
|
||||
cat <&$rfd
|
||||
wait
|
||||
echo "DONE"
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Coproc with compound command bodies
|
||||
#
|
||||
- name: "Coproc running a for loop"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
for i in 1 2 3; do
|
||||
echo "iteration $i"
|
||||
done
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc running a while loop"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
n=1
|
||||
while [ $n -le 3 ]; do
|
||||
echo "count $n"
|
||||
n=$((n + 1))
|
||||
done
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc with conditionals"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
if true; then
|
||||
echo "true branch"
|
||||
else
|
||||
echo "false branch"
|
||||
fi
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc with case statement"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
val="hello"
|
||||
case $val in
|
||||
hello) echo "matched hello";;
|
||||
*) echo "no match";;
|
||||
esac
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc body with pipeline"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
echo -e "banana\napple\ncherry" | sort
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc body with command substitution"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
result=$(echo "hello" | tr a-z A-Z)
|
||||
echo "$result"
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc with arithmetic operations"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
a=5
|
||||
b=3
|
||||
echo $((a + b))
|
||||
echo $((a * b))
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Exit status and lifecycle
|
||||
#
|
||||
- name: "Coproc statement returns 0"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; echo done; }
|
||||
status=$?
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
echo "exit status: $status"
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc with failing body returns 0 immediately"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc { exit 42; }
|
||||
echo "coproc start status: $?"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Wait with no args waits for coproc"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; echo "coproc done"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
echo "after wait"
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Error cases
|
||||
#
|
||||
- name: "Invalid coproc name"
|
||||
ignore_stderr: true
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc 123bad { echo x; }
|
||||
echo "exit: $?"
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Coproc in different contexts
|
||||
#
|
||||
- name: "Coproc inside a function"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
start_coproc() {
|
||||
coproc { read -r _ < _sync; echo "from function coproc"; }
|
||||
}
|
||||
start_coproc
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc inside a conditional"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
if true; then
|
||||
coproc { read -r _ < _sync; echo "conditional coproc"; }
|
||||
fi
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc inside a loop body"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
for i in 1; do
|
||||
coproc { read -r _ < _sync; echo "loop coproc $i"; }
|
||||
done
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Named coproc inside a function"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
f() {
|
||||
coproc FUNCPROC { read -r _ < _sync; echo "named in func"; }
|
||||
}
|
||||
f
|
||||
exec 9<&${FUNCPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Multiple coprocs and variable lifecycle
|
||||
#
|
||||
- name: "Second unnamed coproc replaces first"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; echo "first"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
exec 9<&-
|
||||
wait
|
||||
coproc { read -r _ < _sync; echo "second"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Sequential named coprocs with different names"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc PROC1 { read -r _ < _sync; echo "proc1"; }
|
||||
exec 9<&${PROC1[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
exec 9<&-
|
||||
wait
|
||||
coproc PROC2 { read -r _ < _sync; echo "proc2"; }
|
||||
exec 9<&${PROC2[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Environment and variable scope
|
||||
#
|
||||
- name: "Coproc inherits parent environment"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
export MY_VAR="inherited"
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; echo "MY_VAR=$MY_VAR"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc inherits non-exported variables"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
LOCAL_VAR="visible"
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; echo "LOCAL_VAR=$LOCAL_VAR"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc cannot modify parent variables"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
PARENT_VAR="original"
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; PARENT_VAR="modified"; echo "in coproc: $PARENT_VAR"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
echo "in parent: $PARENT_VAR"
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc inherits functions"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
greet() { echo "hello $1"; }
|
||||
export -f greet
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; greet "world"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc inherits aliases"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
shopt -s expand_aliases
|
||||
alias myecho='echo ALIASED:'
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; myecho "test"; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Coproc with redirections in body
|
||||
#
|
||||
- name: "Coproc body with stderr redirection"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
echo "stdout msg"
|
||||
echo "stderr msg" >&2
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc body writing to a file"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
echo "file content" > /tmp/coproc_test_$$
|
||||
echo "wrote file"
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
cat /tmp/coproc_test_$$
|
||||
rm -f /tmp/coproc_test_$$
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Coproc with various data patterns
|
||||
#
|
||||
- name: "Coproc producing empty output"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc { read -r _ < _sync; true; }
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
echo "after empty coproc"
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc producing multiple lines of output"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
echo "line $i"
|
||||
done
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
count=0
|
||||
while read -r line; do
|
||||
count=$((count + 1))
|
||||
done <&9
|
||||
echo "read $count lines"
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc with multiline string output"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
cat <<'HEREDOC'
|
||||
first line
|
||||
second line
|
||||
third line
|
||||
HEREDOC
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
cat <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Coproc output transformation
|
||||
#
|
||||
- name: "Transform coproc output to uppercase"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
echo "hello"
|
||||
echo "world"
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
while read -r line; do
|
||||
echo "$line" | tr a-z A-Z
|
||||
done <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc output filtered with case"
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
mkfifo _sync
|
||||
coproc {
|
||||
read -r _ < _sync
|
||||
echo "apple"
|
||||
echo "banana"
|
||||
echo "avocado"
|
||||
echo "cherry"
|
||||
}
|
||||
exec 9<&${COPROC[0]}
|
||||
echo x > _sync
|
||||
while read -r line; do
|
||||
case $line in
|
||||
a*) echo "starts with a: $line";;
|
||||
esac
|
||||
done <&9
|
||||
wait
|
||||
args: ["./test.sh"]
|
||||
|
||||
#
|
||||
# Tests requiring wait with job specs (known failures)
|
||||
#
|
||||
- name: "Wait for specific coproc PID"
|
||||
known_failure: true # TODO(wait): wait with job specs not implemented
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc { echo "done"; }
|
||||
fd=${COPROC[0]}
|
||||
cat <&$fd
|
||||
wait $COPROC_PID
|
||||
echo "wait status: $?"
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Coproc with failing body - wait captures exit status"
|
||||
known_failure: true # TODO(wait): wait with job specs not implemented
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc { exit 42; }
|
||||
wait $COPROC_PID
|
||||
echo "exit status: $?"
|
||||
args: ["./test.sh"]
|
||||
|
||||
- name: "Wait for named coproc PID"
|
||||
known_failure: true # TODO(wait): wait with job specs not implemented
|
||||
test_files:
|
||||
- path: "test.sh"
|
||||
contents: |
|
||||
coproc MYPROC { echo "done"; }
|
||||
fd=${MYPROC[0]}
|
||||
cat <&$fd
|
||||
wait $MYPROC_PID
|
||||
echo "status: $?"
|
||||
args: ["./test.sh"]
|
||||
@@ -0,0 +1,195 @@
|
||||
name: "Compound commands: for"
|
||||
cases:
|
||||
- name: "Single-line for loop"
|
||||
stdin: |
|
||||
for f in 1 2 3; do echo $f; done
|
||||
|
||||
- name: "Multi-line for loop"
|
||||
stdin: |
|
||||
for f in 1 2 3; do
|
||||
echo $f
|
||||
done
|
||||
|
||||
- name: "Empty for loop"
|
||||
stdin: |
|
||||
for f in; do echo $f; done
|
||||
|
||||
- name: "for loop without in"
|
||||
stdin: |
|
||||
set -- a b c
|
||||
|
||||
echo "Loop 1"
|
||||
for f; do echo $f; done
|
||||
|
||||
echo "Loop 2: no semicolon"
|
||||
for f do echo $f; done
|
||||
|
||||
- name: "for loop without in but spaces"
|
||||
stdin: |
|
||||
set -- a "b c" d
|
||||
|
||||
echo "Loop 1"
|
||||
for f; do echo $f; done
|
||||
|
||||
echo "Loop 2: no semicolon"
|
||||
for f do echo $f; done
|
||||
|
||||
- name: "Break in for loop"
|
||||
stdin: |
|
||||
for f in 1 2 3; do
|
||||
echo $f
|
||||
break
|
||||
done
|
||||
|
||||
- name: "Break in nested for loop"
|
||||
stdin: |
|
||||
for f in 1 2 3; do
|
||||
for g in a b c; do
|
||||
echo "f=$f g=$g"
|
||||
true && break
|
||||
done
|
||||
echo "Left inner loop"
|
||||
done
|
||||
|
||||
- name: "Break 1 in nested for loops"
|
||||
stdin: |
|
||||
for f in 1 2 3; do
|
||||
for g in a b c; do
|
||||
echo "f=$f g=$g"
|
||||
break 1
|
||||
done
|
||||
done
|
||||
|
||||
- name: "Break 2 in nested for loops"
|
||||
stdin: |
|
||||
for f in 1 2 3; do
|
||||
for g in a b c; do
|
||||
echo "f=$f g=$g"
|
||||
break 2
|
||||
done
|
||||
done
|
||||
|
||||
- name: "Break out of nested for/while loops"
|
||||
stdin: |
|
||||
for f in 1 2 3; do
|
||||
while true; do
|
||||
echo "f=$f"
|
||||
break 2
|
||||
done
|
||||
done
|
||||
|
||||
- name: "Continue in for loop"
|
||||
stdin: |
|
||||
for f in 1 2 3; do
|
||||
echo $f
|
||||
continue
|
||||
echo $f
|
||||
done
|
||||
|
||||
- name: "Continue with bad N"
|
||||
known_failure: true # Fails differently
|
||||
stdin: |
|
||||
for f in 1 2 3; do
|
||||
echo $f
|
||||
continue 0
|
||||
echo result: $?
|
||||
done
|
||||
|
||||
- name: "Continue in nested for loop"
|
||||
stdin: |
|
||||
for f in a b c; do
|
||||
for g in 1 2 3; do
|
||||
echo "f=$f g=$g"
|
||||
continue
|
||||
echo "Should not print"
|
||||
done
|
||||
echo "Left inner loop"
|
||||
done
|
||||
|
||||
- name: "Continue in nested for loop with explicit N=1"
|
||||
stdin: |
|
||||
for f in a b c; do
|
||||
for g in 1 2 3; do
|
||||
echo "f=$f g=$g"
|
||||
continue 1
|
||||
echo "Should not print"
|
||||
done
|
||||
echo "Left inner loop"
|
||||
done
|
||||
|
||||
- name: "Continue in nested for loop with explicit N=2"
|
||||
stdin: |
|
||||
for f in a b c; do
|
||||
for g in 1 2 3; do
|
||||
echo "f=$f g=$g"
|
||||
continue 2
|
||||
echo "Should not print"
|
||||
done
|
||||
echo "Left inner loop"
|
||||
done
|
||||
|
||||
- name: "Continue in nested for loop with too large N"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
for f in a b c; do
|
||||
for g in 1 2 3; do
|
||||
echo "f=$f g=$g"
|
||||
continue 3
|
||||
echo "Should not print"
|
||||
done
|
||||
echo "Left inner loop"
|
||||
done
|
||||
|
||||
- name: "Multi-line for loop"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
for f in 1 2 3; do
|
||||
echo $f
|
||||
done
|
||||
args: ["./script.sh"]
|
||||
|
||||
- name: "For loop piped"
|
||||
stdin: |
|
||||
for f in ab ac bd ef; do echo $f; done | grep b
|
||||
|
||||
- name: "For loop output redirection"
|
||||
stdin: |
|
||||
for f in a b c; do echo $f; done > ./out.txt
|
||||
|
||||
- name: "For reserved word handling"
|
||||
stdin: |
|
||||
for for in for; do echo $for; done
|
||||
|
||||
- name: "For with return in body"
|
||||
stdin: |
|
||||
f() {
|
||||
for i in 1 2 3; do
|
||||
echo "Iteration $i"
|
||||
if [ "$i" = "2" ]; then
|
||||
return 42
|
||||
fi
|
||||
done
|
||||
echo "After for"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "For with exit in body"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Iteration $i"
|
||||
if [ "$i" = "2" ]; then
|
||||
exit 42
|
||||
fi
|
||||
done
|
||||
echo "After for"
|
||||
|
||||
- name: "For loop with nameref variable"
|
||||
stdin: |
|
||||
target=""
|
||||
declare -n ref=target
|
||||
for ref in a b c; do
|
||||
echo "loop: target=$target"
|
||||
done
|
||||
echo "final: target=$target"
|
||||
@@ -0,0 +1,99 @@
|
||||
name: "Compound commands: if"
|
||||
cases:
|
||||
- name: "Basic if"
|
||||
stdin: |
|
||||
if false; then echo 1; else echo 2; fi
|
||||
if false; then echo 3; elif false; then echo 4; else echo 5; fi
|
||||
if true; then echo 6; else echo 7; fi
|
||||
if false; then echo 8; elif true; then echo 10; else echo 11; fi
|
||||
|
||||
- name: "Multi-line if"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
if false; then
|
||||
echo 1
|
||||
else
|
||||
echo 2
|
||||
fi
|
||||
|
||||
if false; then
|
||||
echo 3
|
||||
elif false; then
|
||||
echo 4
|
||||
else
|
||||
echo 5
|
||||
fi
|
||||
|
||||
if true; then
|
||||
echo 6
|
||||
else
|
||||
echo 7
|
||||
fi
|
||||
|
||||
if false; then
|
||||
echo 8
|
||||
elif true; then
|
||||
echo 10
|
||||
else
|
||||
echo 11
|
||||
fi
|
||||
args: ["./script.sh"]
|
||||
|
||||
- name: "If with return in condition"
|
||||
stdin: |
|
||||
f() {
|
||||
if return 42; then
|
||||
echo "Then branch"
|
||||
else
|
||||
echo "Else branch"
|
||||
fi
|
||||
echo "After if"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "If with exit in condition"
|
||||
stdin: |
|
||||
if exit 42; then
|
||||
echo "Then branch"
|
||||
else
|
||||
echo "Else branch"
|
||||
fi
|
||||
echo "After if"
|
||||
|
||||
- name: "If with return in elif condition"
|
||||
stdin: |
|
||||
f() {
|
||||
if false; then
|
||||
echo "Then branch"
|
||||
elif return 42; then
|
||||
echo "Elif then branch"
|
||||
else
|
||||
echo "Else branch"
|
||||
fi
|
||||
echo "After if"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "If with break in condition inside loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
if break; then
|
||||
echo "Then branch"
|
||||
fi
|
||||
echo "After if"
|
||||
done
|
||||
echo "After loop"
|
||||
|
||||
- name: "If with continue in condition inside loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Iteration $i"
|
||||
if continue; then
|
||||
echo "Then branch"
|
||||
fi
|
||||
echo "After if"
|
||||
done
|
||||
echo "After loop"
|
||||
@@ -0,0 +1,10 @@
|
||||
name: "Select statement"
|
||||
cases:
|
||||
- name: "basic"
|
||||
known_failure: true # TODO(select): select statement not implemented
|
||||
stdin: |
|
||||
echo "1" | select opt in a b c; do
|
||||
echo "selected: $opt"
|
||||
break
|
||||
done
|
||||
echo "after select"
|
||||
@@ -0,0 +1,38 @@
|
||||
name: "Compound commands: subshell"
|
||||
cases:
|
||||
- name: "Basic subshell usage"
|
||||
stdin: |
|
||||
(echo hi)
|
||||
|
||||
- name: "Subshells in sequence"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
(echo hi)(echo there)
|
||||
|
||||
- name: "Subshell env usage"
|
||||
stdin: |
|
||||
(subshell_var=value && echo "subshell_var: ${subshell_var}")
|
||||
echo "subshell_var: ${subshell_var}"
|
||||
|
||||
- name: "Subshell output redirection"
|
||||
stdin: |
|
||||
(echo Hello; echo world) >out.txt
|
||||
echo "Dumping out.txt..."
|
||||
cat out.txt
|
||||
|
||||
- name: "Piped subshell usage"
|
||||
stdin: |
|
||||
(echo hi) | wc -l
|
||||
|
||||
- name: "Breaks in subshell"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo $i
|
||||
(for i in 1 2 3; do break 2; done)
|
||||
done
|
||||
|
||||
- name: "Exec in subshell"
|
||||
stdin: |
|
||||
echo "Before subshell"
|
||||
(exec cat <<<"Hello from exec in subshell")
|
||||
echo "After subshell"
|
||||
@@ -0,0 +1,101 @@
|
||||
name: "Compound commands: until"
|
||||
cases:
|
||||
- name: "Single-line until loop"
|
||||
stdin: |
|
||||
until true; do echo 'In loop'; done
|
||||
|
||||
- name: "Arithmetic in until loop"
|
||||
incompatible_configs: ["sh"]
|
||||
stdin: |
|
||||
i=5
|
||||
until ((i == 0)); do echo $i; i=$((i - 1)); done
|
||||
|
||||
- name: "Until with return in condition"
|
||||
stdin: |
|
||||
f() {
|
||||
until return 42; do
|
||||
echo "Body"
|
||||
done
|
||||
echo "After until"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "Until with exit in condition"
|
||||
stdin: |
|
||||
until exit 42; do
|
||||
echo "Body"
|
||||
done
|
||||
echo "After until"
|
||||
|
||||
- name: "Until with break in condition inside outer loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Outer iteration $i"
|
||||
until break; do
|
||||
echo "Until body"
|
||||
done
|
||||
echo "After until"
|
||||
done
|
||||
echo "After outer loop"
|
||||
|
||||
- name: "Until with break 2 in condition inside outer loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Outer iteration $i"
|
||||
until break 2; do
|
||||
echo "Until body"
|
||||
done
|
||||
echo "After until"
|
||||
done
|
||||
echo "After outer loop"
|
||||
|
||||
- name: "Until with continue 2 in condition inside outer loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Outer iteration $i"
|
||||
until continue 2; do
|
||||
echo "Until body"
|
||||
done
|
||||
echo "After until"
|
||||
done
|
||||
echo "After outer loop"
|
||||
|
||||
- name: "Until with continue in condition inside outer loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Outer iteration $i"
|
||||
until continue; do
|
||||
echo "Until body"
|
||||
done
|
||||
echo "After until"
|
||||
done
|
||||
echo "After outer loop"
|
||||
|
||||
- name: "Until with return in body"
|
||||
stdin: |
|
||||
f() {
|
||||
i=0
|
||||
until [ $i -ge 3 ]; do
|
||||
echo "Iteration $i"
|
||||
if [ $i -eq 1 ]; then
|
||||
return 42
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo "After until"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "Until with exit in body"
|
||||
stdin: |
|
||||
i=0
|
||||
until [ $i -ge 3 ]; do
|
||||
echo "Iteration $i"
|
||||
if [ $i -eq 1 ]; then
|
||||
exit 42
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo "After until"
|
||||
@@ -0,0 +1,127 @@
|
||||
name: "Compound commands: while"
|
||||
cases:
|
||||
- name: "Single-line while loop"
|
||||
stdin: |
|
||||
while false; do echo 'In loop'; done
|
||||
|
||||
- name: "break in while loop"
|
||||
stdin: |
|
||||
while true; do
|
||||
echo 'In loop'
|
||||
break
|
||||
done
|
||||
|
||||
- name: "break 2 in nested loops"
|
||||
stdin: |
|
||||
while false; do
|
||||
echo 'Starting inner loop'
|
||||
while true; do
|
||||
echo 'In loop'
|
||||
break 2
|
||||
done
|
||||
echo 'Finished inner loop'
|
||||
done
|
||||
|
||||
- name: "Arithmetic in while loop"
|
||||
stdin: |
|
||||
i=5
|
||||
while ((i > 0)); do echo $i; i=$((i - 1)); done
|
||||
|
||||
- name: "Alternative arithmetic in while loop"
|
||||
stdin: |
|
||||
c=0
|
||||
limit=4
|
||||
while [ $c -lt $limit ]; do
|
||||
case "$c" in
|
||||
0)
|
||||
echo "0"
|
||||
;;
|
||||
1)
|
||||
echo "1"
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
((c++))
|
||||
done
|
||||
echo "Done"
|
||||
|
||||
- name: "While with return in condition"
|
||||
stdin: |
|
||||
f() {
|
||||
while return 42; do
|
||||
echo "Body"
|
||||
done
|
||||
echo "After while"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "While with exit in condition"
|
||||
stdin: |
|
||||
while exit 42; do
|
||||
echo "Body"
|
||||
done
|
||||
echo "After while"
|
||||
|
||||
- name: "While with break in condition inside outer loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Outer iteration $i"
|
||||
while break; do
|
||||
echo "While body"
|
||||
done
|
||||
echo "After while"
|
||||
done
|
||||
echo "After outer loop"
|
||||
|
||||
- name: "While with break 2 in condition inside outer loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Outer iteration $i"
|
||||
while break 2; do
|
||||
echo "While body"
|
||||
done
|
||||
echo "After while"
|
||||
done
|
||||
echo "After outer loop"
|
||||
|
||||
- name: "While with continue 2 in condition inside outer loop"
|
||||
stdin: |
|
||||
for i in 1 2 3; do
|
||||
echo "Outer iteration $i"
|
||||
while continue 2; do
|
||||
echo "While body"
|
||||
done
|
||||
echo "After while"
|
||||
done
|
||||
echo "After outer loop"
|
||||
|
||||
- name: "While with return in body"
|
||||
stdin: |
|
||||
f() {
|
||||
i=0
|
||||
while [ $i -lt 3 ]; do
|
||||
echo "Iteration $i"
|
||||
if [ $i -eq 1 ]; then
|
||||
return 42
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo "After while"
|
||||
}
|
||||
f
|
||||
echo "Exit code: $?"
|
||||
|
||||
- name: "While with exit in body"
|
||||
stdin: |
|
||||
i=0
|
||||
while [ $i -lt 3 ]; do
|
||||
echo "Iteration $i"
|
||||
if [ $i -eq 1 ]; then
|
||||
exit 42
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo "After while"
|
||||
@@ -0,0 +1,399 @@
|
||||
name: "Error handling"
|
||||
cases:
|
||||
- name: "Fatal error (interactive/subshell)"
|
||||
ignore_stderr: true
|
||||
args: ["-i"]
|
||||
stdin: |
|
||||
echo "before expansion"
|
||||
(echo "${non_existent_var:?missing parameter}")
|
||||
echo "after expansion"
|
||||
|
||||
- name: "Fatal error (non-interactive/subshell)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before expansion"
|
||||
(echo "${non_existent_var:?missing parameter}")
|
||||
echo "after expansion"
|
||||
|
||||
- name: "Fatal error (interactive/command substitution)"
|
||||
ignore_stderr: true
|
||||
args: ["-i"]
|
||||
stdin: |
|
||||
echo "before expansion"
|
||||
x=$(echo "${non_existent_var:?missing parameter}")
|
||||
echo "after expansion"
|
||||
|
||||
- name: "Fatal error (non-interactive/command substitution)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before expansion"
|
||||
x=$(echo "${non_existent_var:?missing parameter}")
|
||||
echo "after expansion"
|
||||
|
||||
- name: "Fatal error (interactive/sourced file)"
|
||||
ignore_stderr: true
|
||||
args: ["-i"]
|
||||
test_files:
|
||||
- path: error.sh
|
||||
contents: |
|
||||
echo "before expansion"
|
||||
echo "${non_existent_var:?missing parameter}"
|
||||
echo "after expansion"
|
||||
stdin: |
|
||||
source error.sh
|
||||
echo "after sourcing script with error"
|
||||
|
||||
- name: "Fatal error (non-interactive/sourced file)"
|
||||
ignore_stderr: true
|
||||
test_files:
|
||||
- path: error.sh
|
||||
contents: |
|
||||
echo "before expansion"
|
||||
echo "${non_existent_var:?missing parameter}"
|
||||
echo "after expansion"
|
||||
stdin: |
|
||||
source error.sh
|
||||
echo "after sourcing script with error"
|
||||
|
||||
- name: "Fatal error (interactive/executed script)"
|
||||
ignore_stderr: true
|
||||
args: ["-i", "error.sh"]
|
||||
test_files:
|
||||
- path: error.sh
|
||||
contents: |
|
||||
echo "before expansion"
|
||||
echo "${non_existent_var:?missing parameter}"
|
||||
echo "after expansion"
|
||||
|
||||
- name: "Fatal error (non-interactive/executed script)"
|
||||
ignore_stderr: true
|
||||
args: ["error.sh"]
|
||||
test_files:
|
||||
- path: error.sh
|
||||
contents: |
|
||||
echo "before expansion"
|
||||
echo "${non_existent_var:?missing parameter}"
|
||||
echo "after expansion"
|
||||
|
||||
- name: "Shell language syntax error (command line)"
|
||||
incompatible_os: ["azurelinux"] # Needs investigation
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
[[ -% 3 ]]
|
||||
echo "after syntax error"
|
||||
|
||||
- name: "Shell language syntax error (non-interactive)"
|
||||
incompatible_os: ["azurelinux"] # Needs investigation
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
[[ -% 3 ]]
|
||||
echo "after syntax error"
|
||||
|
||||
- name: "Shell language syntax error (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
[[ -% 3 ]]
|
||||
echo "after syntax error"
|
||||
|
||||
- name: "Special built-in error (command line)"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
echo "before shift"
|
||||
set -- foo
|
||||
shift 2
|
||||
echo "after shift"
|
||||
|
||||
- name: "Special built-in error (non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before shift"
|
||||
set -- foo
|
||||
shift 2
|
||||
echo "after shift"
|
||||
|
||||
- name: "Special built-in error (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before shift"
|
||||
set -- foo
|
||||
shift 2
|
||||
echo "after shift"
|
||||
|
||||
- name: "Regular utility error (command line)"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
echo "before cat"
|
||||
cat /no/such/utility/path 2>/dev/null
|
||||
echo "after cat (status $?)"
|
||||
|
||||
- name: "Regular utility error (non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before cat"
|
||||
cat /no/such/utility/path 2>/dev/null
|
||||
echo "after cat (status $?)"
|
||||
|
||||
- name: "Regular utility error (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before cat"
|
||||
cat /no/such/utility/path 2>/dev/null
|
||||
echo "after cat (status $?)"
|
||||
|
||||
- name: "Special built-in redirection error (command line)"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
echo "before special redirection"
|
||||
: > missing_special_dir/out.txt
|
||||
echo "after special redirection"
|
||||
|
||||
- name: "Special built-in redirection error (non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before special redirection"
|
||||
: > missing_special_dir/out.txt
|
||||
echo "after special redirection"
|
||||
|
||||
- name: "Special built-in redirection error (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before special redirection"
|
||||
: > missing_special_dir/out.txt
|
||||
echo "after special redirection"
|
||||
|
||||
- name: "Compound command redirection error (command line)"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
echo "before compound redirection"
|
||||
if true; then echo inside; fi > missing_compound_dir/out.txt
|
||||
echo "after compound redirection"
|
||||
|
||||
- name: "Compound command redirection error (non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before compound redirection"
|
||||
if true; then echo inside; fi > missing_compound_dir/out.txt
|
||||
echo "after compound redirection"
|
||||
|
||||
- name: "Compound command redirection error (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before compound redirection"
|
||||
if true; then echo inside; fi > missing_compound_dir/out.txt
|
||||
echo "after compound redirection"
|
||||
|
||||
- name: "Function redirection error (command line)"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
demo_func() { echo inside; }
|
||||
echo "before function redirection"
|
||||
demo_func > missing_function_dir/out.txt
|
||||
echo "after function redirection"
|
||||
|
||||
- name: "Function redirection error (non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
demo_func() { echo inside; }
|
||||
echo "before function redirection"
|
||||
demo_func > missing_function_dir/out.txt
|
||||
echo "after function redirection"
|
||||
|
||||
- name: "Function redirection error (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
demo_func() { echo inside; }
|
||||
echo "before function redirection"
|
||||
demo_func > missing_function_dir/out.txt
|
||||
echo "after function redirection"
|
||||
|
||||
- name: "Utility redirection error (command line)"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
echo "before cat redirection"
|
||||
cat < missing_utility_dir/input.txt
|
||||
echo "after cat redirection"
|
||||
|
||||
- name: "Utility redirection error (non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before cat redirection"
|
||||
cat < missing_utility_dir/input.txt
|
||||
echo "after cat redirection"
|
||||
|
||||
- name: "Utility redirection error (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before cat redirection"
|
||||
cat < missing_utility_dir/input.txt
|
||||
echo "after cat redirection"
|
||||
|
||||
- name: "Variable assignment error (command line)"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
readonly value_guard=1
|
||||
echo "before readonly assignment"
|
||||
value_guard=2
|
||||
echo "after readonly assignment"
|
||||
|
||||
- name: "Variable assignment error (non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
readonly value_guard=1
|
||||
echo "before readonly assignment"
|
||||
value_guard=2
|
||||
echo "after readonly assignment"
|
||||
|
||||
- name: "Variable assignment error (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
readonly value_guard=1
|
||||
echo "before readonly assignment"
|
||||
value_guard=2
|
||||
echo "after readonly assignment"
|
||||
|
||||
- name: "Expansion error (command line)"
|
||||
known_failure: true # Needs investigation
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
echo "before expansion"
|
||||
echo "${non_existent_var:?missing parameter}"
|
||||
echo "after expansion"
|
||||
|
||||
- name: "Expansion error (non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before expansion"
|
||||
echo "${UNSET_REQUIRED:?missing parameter}"
|
||||
echo "after expansion"
|
||||
|
||||
- name: "Expansion error (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before expansion"
|
||||
echo "${UNSET_REQUIRED:?missing parameter}"
|
||||
echo "after expansion"
|
||||
|
||||
- name: "Command not found (command line)"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
missing_command
|
||||
echo "after missing command (status $?)"
|
||||
|
||||
- name: "Command not found (non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
missing_command
|
||||
echo "after missing command (status $?)"
|
||||
|
||||
- name: "Command not found (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
missing_command
|
||||
echo "after missing command (status $?)"
|
||||
|
||||
- name: "Unrecoverable read error (command line)"
|
||||
ignore_stderr: true
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
echo "before closing stdin"
|
||||
exec 0<&-
|
||||
echo "after closing stdin"
|
||||
|
||||
- name: "Unrecoverable read error (non-interactive)"
|
||||
known_failure: true
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before closing stdin"
|
||||
exec 0<&-
|
||||
echo "after closing stdin"
|
||||
|
||||
- name: "Unrecoverable read error (interactive)"
|
||||
known_failure: true
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before closing stdin"
|
||||
exec 0<&-
|
||||
echo "after closing stdin"
|
||||
|
||||
- name: "Failing special built-in (non-POSIX mode, non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
. ./non-existent-file
|
||||
echo "after builtin call (status $?)"
|
||||
|
||||
- name: "Failing special built-in (non-POSIX mode, interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
. ./non-existent-file
|
||||
echo "after builtin call (status $?)"
|
||||
|
||||
- name: "Failing special built-in (POSIX mode, non-interactive)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
set -o posix
|
||||
. ./non-existent-file
|
||||
echo "after builtin call (status $?)"
|
||||
|
||||
- name: "Failing special built-in (POSIX mode, interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
set -o posix
|
||||
. ./non-existent-file
|
||||
echo "after builtin call (status $?)"
|
||||
|
||||
- name: "errexit with failing command (non-interactive)"
|
||||
stdin: |
|
||||
set -e
|
||||
false
|
||||
echo "after false"
|
||||
|
||||
- name: "errexit with failing command (interactive)"
|
||||
args: ["-i"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
set -e
|
||||
echo "before false"
|
||||
false
|
||||
echo "after false"
|
||||
|
||||
- name: "errexit with unbound variable access (interactive)"
|
||||
args: ["-i", "-e", "-u", "-o", "pipefail"]
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
echo "before false"
|
||||
false
|
||||
echo "after false"
|
||||
@@ -0,0 +1,434 @@
|
||||
name: "Extended tests"
|
||||
cases:
|
||||
- name: "Extended test with redirect"
|
||||
stdin: |
|
||||
[[ $(echo hello >&2) ]] 2>/dev/null
|
||||
echo "Result: $?"
|
||||
|
||||
- name: "File extended tests"
|
||||
stdin: |
|
||||
[[ -a /tmp ]] && echo "-a correctly checked /tmp"
|
||||
[[ -a /some/non/existent/path ]] || echo "-a correctly checked non-existent path"
|
||||
|
||||
[[ -c /dev/null ]] && echo "-c correctly checked /dev/null"
|
||||
[[ -c /tmp ]] || echo "-c correctly checked /tmp"
|
||||
|
||||
- name: "Existence tests"
|
||||
stdin: |
|
||||
[[ -e '' ]] || echo "-e correctly identified empty string as non-existent"
|
||||
|
||||
[[ -e non-existent ]] || echo "-e correctly identified non-existent path"
|
||||
[[ ! -e non-existent ]] && echo "! -e correctly identified non-existent path"
|
||||
|
||||
touch test-file
|
||||
[[ -e test-file ]] && echo "-e correctly identified existing non-dir"
|
||||
|
||||
mkdir test-dir
|
||||
[[ -e test-dir ]] && echo "-d correctly identified existing dir"
|
||||
|
||||
- name: "Directory tests"
|
||||
stdin: |
|
||||
[[ -d '' ]] || echo "-d correctly identified empty string as non-existent"
|
||||
|
||||
[[ -d non-existent ]] || echo "-d correctly identified non-existent path"
|
||||
[[ ! -d non-existent ]] && echo "! -d correctly identified non-existent path"
|
||||
|
||||
touch test-file
|
||||
[[ -d test-file ]] || echo "-d correctly identified non-dir file"
|
||||
|
||||
mkdir test-dir
|
||||
[[ -d test-dir ]] && echo "-d correctly identified existing dir"
|
||||
|
||||
- name: "File regular tests"
|
||||
stdin: |
|
||||
[[ -f '' ]] || echo "-f correctly identified empty string as non-existent"
|
||||
|
||||
[[ -f non-existent ]] || echo "-f correctly identified non-existent path"
|
||||
[[ ! -f non-existent ]] && echo "! -f correctly identified non-existent path"
|
||||
|
||||
touch test-file
|
||||
[[ -f test-file ]] && echo "-f correctly identified regular file"
|
||||
|
||||
mkdir test-dir
|
||||
[[ -f test-dir ]] || echo "-f correctly identified directory as non-regular file"
|
||||
|
||||
- name: "File symbolic link tests"
|
||||
stdin: |
|
||||
[[ -L non-existent ]] || echo "-L correctly identified non-existent path"
|
||||
|
||||
touch test-file
|
||||
[[ -L test-file ]] || echo "-L correctly identified non-link file"
|
||||
|
||||
ln -s test-file valid-link
|
||||
[[ -L valid-link ]] && echo "-L correctly identified valid symbolic link"
|
||||
|
||||
ln -s non-existent-target dangling-link
|
||||
[[ -L dangling-link ]] && echo "-L correctly identified dangling symbolic link"
|
||||
|
||||
- name: "File sticky bit tests"
|
||||
stdin: |
|
||||
touch test-file
|
||||
[[ -k test-file ]] || echo "-k correctly identified file without sticky bit"
|
||||
|
||||
chmod 1600 test-file
|
||||
[[ -k test-file ]] && echo "-k correctly identified file with sticky bit"
|
||||
|
||||
- name: "Fifo test"
|
||||
stdin: |
|
||||
touch test-file
|
||||
[[ -p test-file ]] || echo "-p correctly identified non-fifo file"
|
||||
|
||||
mkfifo fifo-file
|
||||
[[ -p test-file ]] && echo "-p correctly identified fifo file"
|
||||
|
||||
- name: "File executable tests"
|
||||
stdin: |
|
||||
[[ -x non-existent ]] || echo "-x correctly identified non-existent path"
|
||||
|
||||
touch test-file
|
||||
ln -sf link test-file
|
||||
[[ -x test-file ]] || echo "-x correctly identified non-executable file"
|
||||
[[ -x link ]] || echo "-x correctly identified link to non-executable file"
|
||||
|
||||
chmod o+x test-file
|
||||
[[ -x test-file ]] || echo "-x correctly identified other-only-executable file"
|
||||
[[ -x link ]] || echo "-x correctly identified link to other-only-executable file"
|
||||
|
||||
chmod a+x test-file
|
||||
[[ -x test-file ]] && echo "-x correctly identified executable file"
|
||||
[[ -x link ]] && echo "-x correctly identified link to executable file"
|
||||
|
||||
- name: "File writable tests"
|
||||
stdin: |
|
||||
[[ -w non-existent ]] || echo "-w correctly identified non-existent path"
|
||||
|
||||
touch test-file
|
||||
ln -sf link test-file
|
||||
[[ -w test-file ]] || echo "-w correctly identified non-writable file"
|
||||
[[ -w link ]] || echo "-w correctly identified link to non-writable file"
|
||||
|
||||
chmod o+x test-file
|
||||
[[ -w test-file ]] || echo "-w correctly identified other-only-writable file"
|
||||
[[ -w link ]] || echo "-w correctly identified link to other-only-writable file"
|
||||
|
||||
chmod a+x test-file
|
||||
[[ -w test-file ]] && echo "-w correctly identified writable file"
|
||||
[[ -w link ]] && echo "-w correctly identified link to writable file"
|
||||
|
||||
- name: "Variable set and nameref tests"
|
||||
stdin: |
|
||||
foo="bar"
|
||||
declare -n bang=foo
|
||||
[[ -R bang ]] && echo "-R correctly identified nameref variable that is set"
|
||||
|
||||
declare -n somevar
|
||||
[[ -R somevar ]] || echo "-R correctly identified nameref variable that isn't set"
|
||||
|
||||
- name: "Files refer to same device and inode tests"
|
||||
stdin: |
|
||||
[[ /bin/sh -ef /bin/sh ]] && echo "-ef correctly identified device and inode numbers"
|
||||
|
||||
[[ ! /etc/os-release -ef /bin/sh ]] && echo "-ef correctly identified device and inode numbers that do not match"
|
||||
|
||||
- name: "File is newer"
|
||||
stdin: |
|
||||
touch -d "2 hours ago" bar
|
||||
touch foo
|
||||
|
||||
[[ foo -nt bar ]] && echo "-nt correctly identified newer file"
|
||||
[[ foo -nt foo ]] && echo "-nt incorrectly identified file as newer than itself"
|
||||
[[ foo -nt file_no_exists ]] && echo "-nt correctly identified when file2 does not exist"
|
||||
|
||||
- name: "File is older"
|
||||
stdin: |
|
||||
touch -d "2 hours ago" foo
|
||||
touch bar
|
||||
|
||||
[[ foo -ot bar ]] && echo "-ot correctly identified older file"
|
||||
[[ foo -ot foo ]] && echo "-ot incorrectly identified file as older than itself"
|
||||
[[ file_no_exists -ot foo ]] && echo "-ot correctly identified when file1 does not exist"
|
||||
|
||||
- name: "Unary string extended tests"
|
||||
stdin: |
|
||||
[[ -z "" ]] && echo "-z: Pass"
|
||||
[[ -z "something" ]] && echo "-z: Fail"
|
||||
|
||||
[[ -n "something" ]] && echo "-n: Pass"
|
||||
[[ -n "" ]] && echo "-n: Fail"
|
||||
|
||||
- name: "Shell option extended tests"
|
||||
stdin: |
|
||||
set -o emacs
|
||||
[[ -o emacs ]] && echo "1: option enabled"
|
||||
|
||||
set +o emacs
|
||||
[[ -o emacs ]] && echo "2: option enabled"
|
||||
|
||||
- name: "Binary string extended tests"
|
||||
stdin: |
|
||||
[[ "" == "" ]] && echo "1. Pass"
|
||||
[[ "" = "" ]] && echo "2. Pass"
|
||||
[[ "" != "" ]] && echo "3. Fail"
|
||||
|
||||
[[ "a" != "b" ]] && echo "4. Pass"
|
||||
[[ "a" = "b" ]] && echo "5. Fail"
|
||||
[[ "a" == "b" ]] && echo "6. Fail"
|
||||
|
||||
[[ "a" < "b" ]] && echo "7. Pass"
|
||||
[[ "a" < "a" ]] && echo "8. Fail"
|
||||
[[ "b" < "a" ]] && echo "9. Fail"
|
||||
[[ "a" > "b" ]] && echo "10. Fail"
|
||||
[[ "a" > "a" ]] && echo "11. Fail"
|
||||
[[ "b" > "a" ]] && echo "12. Pass"
|
||||
|
||||
- name: "Binary string matching"
|
||||
stdin: |
|
||||
[[ "abc" == a* ]] && echo "1. Pass"
|
||||
[[ "abc" != a* ]] && echo "2. Fail"
|
||||
[[ a* != "abc" ]] && echo "3. Pass"
|
||||
|
||||
- name: "Binary string matching with expansion"
|
||||
stdin: |
|
||||
exclude="0123456789"
|
||||
[[ "clue" == +([$exclude]) ]] && echo "1. Fail"
|
||||
[[ "8675309" == +([$exclude]) ]] && echo "2. Pass"
|
||||
|
||||
- name: "Quoted pattern binary string matching"
|
||||
stdin: |
|
||||
[[ "abc" == "a*" ]] && echo "1. Matches"
|
||||
[[ "abc" != "a*" ]] && echo "2. Matches"
|
||||
|
||||
- name: "Tilde binary string matching"
|
||||
stdin: |
|
||||
x='~/'
|
||||
[[ $x == ~* ]] && echo "1. Matches"
|
||||
|
||||
- name: "Arithmetic extended tests"
|
||||
stdin: |
|
||||
[[ 0 -eq 0 ]] && echo "1. Pass"
|
||||
[[ 0 -ne 0 ]] && echo "2. Fail"
|
||||
[[ 0 -lt 0 ]] && echo "3. Fail"
|
||||
[[ 0 -le 0 ]] && echo "4. Pass"
|
||||
[[ 0 -gt 0 ]] && echo "5. Fail"
|
||||
[[ 0 -ge 0 ]] && echo "6. Pass"
|
||||
|
||||
[[ 0 -eq 1 ]] && echo "7. Fail"
|
||||
[[ 0 -ne 1 ]] && echo "8. Pass"
|
||||
[[ 0 -lt 1 ]] && echo "9. Pass"
|
||||
[[ 0 -le 1 ]] && echo "10. Pass"
|
||||
[[ 0 -gt 1 ]] && echo "11. Fail"
|
||||
[[ 0 -ge 1 ]] && echo "12. Fail"
|
||||
|
||||
[[ 1 -eq 0 ]] && echo "13. Fail"
|
||||
[[ 1 -ne 0 ]] && echo "14. Pass"
|
||||
[[ 1 -lt 0 ]] && echo "15. Fail"
|
||||
[[ 1 -le 0 ]] && echo "16. Fail"
|
||||
[[ 1 -gt 0 ]] && echo "17. Pass"
|
||||
[[ 1 -ge 0 ]] && echo "18. Pass"
|
||||
|
||||
- name: "Regex"
|
||||
stdin: |
|
||||
[[ "a" =~ ^a$ ]] && echo "1. Pass"
|
||||
[[ "abc" =~ a* ]] && echo "2. Pass"
|
||||
[[ a =~ ^(a)$ ]] && echo "3. Pass"
|
||||
[[ a =~ ^(a|b)$ ]] && echo "4. Pass"
|
||||
[[ a =~ c ]] && echo "5. Pass"
|
||||
|
||||
- name: "Regex with case insensitivity"
|
||||
stdin: |
|
||||
shopt -u nocasematch
|
||||
[[ "a" =~ A ]] && echo "1. Pass"
|
||||
|
||||
shopt -s nocasematch
|
||||
[[ "a" =~ A ]] && echo "1. Pass"
|
||||
|
||||
- name: "Regex with capture"
|
||||
stdin: |
|
||||
pattern='(Hello), ([a-z]+)\.'
|
||||
if [[ "Hello, world." =~ ${pattern} ]]; then
|
||||
echo "Match found!"
|
||||
for i in "${!BASH_REMATCH[@]}"; do
|
||||
echo "$i: '${BASH_REMATCH[$i]}'"
|
||||
done
|
||||
fi
|
||||
|
||||
- name: "Regex with capture including optional matches"
|
||||
stdin: |
|
||||
pattern='(Hello)(,?) ([a-z]+)\.'
|
||||
if [[ "Hello world." =~ ${pattern} ]]; then
|
||||
echo "Match found!"
|
||||
for i in "${!BASH_REMATCH[@]}"; do
|
||||
echo "$i: '${BASH_REMATCH[$i]}'"
|
||||
done
|
||||
fi
|
||||
|
||||
- name: "Regex with quoting"
|
||||
stdin: |
|
||||
regex="^$"
|
||||
|
||||
# TODO(test): The commented out lines appear differ in behavior between versions of bash.
|
||||
# [[ "" =~ '' ]] && echo "1. Matched"
|
||||
[[ "" =~ '^$' ]] && echo "2. Matched"
|
||||
# [[ "" =~ "" ]] && echo "3. Matched"
|
||||
[[ "" =~ "^$" ]] && echo "4. Matched"
|
||||
[[ "" =~ ^$ ]] && echo "5. Matched"
|
||||
[[ "" =~ ${regex} ]] && echo "6. Matched"
|
||||
[[ "" =~ "${regex}" ]] && echo "7. Matched"
|
||||
|
||||
- name: "Regex with escaping"
|
||||
stdin: |
|
||||
[[ '' =~ ^\$$ ]] && echo "1. Matched"
|
||||
|
||||
- name: "Regex with double parens"
|
||||
stdin: |
|
||||
if [[ xy =~ x+((y)) ]]; then
|
||||
echo "Matches"
|
||||
fi
|
||||
|
||||
- name: "Regex with special chars in parens"
|
||||
stdin: |
|
||||
[[ "<" =~ (<) ]] && echo "1. Matched"
|
||||
[[ ">" =~ (<) ]] && echo "2. Matched"
|
||||
|
||||
- name: "Regex with spaces inside parens"
|
||||
stdin: |
|
||||
[[ "x" =~ (a| *) ]] && echo "1. Matched"
|
||||
[[ "a b" =~ ^(a |b) ]] && echo "2. Matched: [${BASH_REMATCH[0]}]"
|
||||
[[ "a b" =~ ^(a |b)$ ]] || echo "3. No match"
|
||||
[[ " " =~ ^( )$ ]] && echo "4. Matched: [${BASH_REMATCH[1]}]"
|
||||
|
||||
- name: "Regex with unescaped open bracket in character class"
|
||||
stdin: |
|
||||
[[ "[" =~ ^([x[]) ]] && echo "Matched"
|
||||
|
||||
- name: "Empty and space checks"
|
||||
stdin: |
|
||||
check() {
|
||||
var="$1"
|
||||
[[ ${var} && ! ${var//[[:space:]]/} ]]
|
||||
}
|
||||
|
||||
check "" && echo "1. Only space"
|
||||
check " " && echo "2. Only space"
|
||||
check $'\t' && echo "3. Only space"
|
||||
check " a " && echo "4. Only space"
|
||||
|
||||
- name: "Newlines in test expression"
|
||||
stdin: |
|
||||
[[
|
||||
"a" == "a"
|
||||
&&
|
||||
"b" == "b"
|
||||
]] && echo "Succeeded"
|
||||
|
||||
- name: "Variable set checks"
|
||||
stdin: |
|
||||
declare set_but_no_value
|
||||
[[ -v set_but_no_value ]] && echo "1. Set but no value"
|
||||
|
||||
declare set_with_value=xyz
|
||||
[[ -v set_with_value ]] && echo "2. Set with value"
|
||||
|
||||
[[ -v not_set ]] || echo "3. Not set"
|
||||
|
||||
- name: "Variables in extended tests"
|
||||
stdin: |
|
||||
var=10
|
||||
|
||||
[[ $var -eq 10 ]] && echo "1. Pass"
|
||||
[[ var -eq 10 ]] && echo "2. Pass"
|
||||
|
||||
- name: "Regex with min/max counts"
|
||||
stdin: |
|
||||
[[ z =~ ^z{2,6}$ ]] && echo "1. Matches"
|
||||
[[ zzzz =~ ^z{2,6}$ ]] && echo "2. Matches"
|
||||
[[ zzzzzzzzz =~ ^z{2,6}$ ]] && echo "3. Matches"
|
||||
|
||||
- name: "Regex with newline"
|
||||
stdin: |
|
||||
[[ $'\n' =~ . ]] && echo "1. Matches"
|
||||
|
||||
- name: "Arithmetic comparisons with whitespace in operands"
|
||||
stdin: |
|
||||
# Leading/trailing whitespace should be trimmed before comparison
|
||||
[[ 0 -eq ' 0' ]] && echo "1. leading space"
|
||||
[[ 0 -eq '0 ' ]] && echo "2. trailing space"
|
||||
[[ 0 -eq ' 0 ' ]] && echo "3. both spaces"
|
||||
|
||||
# Tabs
|
||||
[[ 0 -eq ' 0' ]] && echo "4. leading tab"
|
||||
[[ 0 -eq '0 ' ]] && echo "5. trailing tab"
|
||||
|
||||
# Newlines
|
||||
[[ 0 -eq ' 0
|
||||
' ]] && echo "6. newline"
|
||||
|
||||
# Other operators
|
||||
[[ 5 -lt ' 10' ]] && echo "7. -lt with space"
|
||||
[[ 10 -gt ' 5' ]] && echo "8. -gt with space"
|
||||
[[ 5 -ne ' 6' ]] && echo "9. -ne with space"
|
||||
[[ 5 -le ' 5' ]] && echo "10. -le with space"
|
||||
[[ 5 -ge ' 5' ]] && echo "11. -ge with space"
|
||||
|
||||
# Negative numbers with whitespace
|
||||
[[ -5 -eq ' -5' ]] && echo "12. negative with space"
|
||||
|
||||
- name: "Terminal fd check with whitespace"
|
||||
stdin: |
|
||||
# -t operator should trim whitespace before parsing fd number
|
||||
# These should all behave identically (false since not a terminal)
|
||||
[[ -t 0 ]]
|
||||
r1=$?
|
||||
[[ -t ' 0' ]]
|
||||
r2=$?
|
||||
[[ -t '0 ' ]]
|
||||
r3=$?
|
||||
|
||||
# All should have the same result
|
||||
[[ $r1 -eq $r2 && $r2 -eq $r3 ]] && echo "All -t results match"
|
||||
|
||||
- name: "Nameref -R with attribute removal"
|
||||
stdin: |
|
||||
target="value"
|
||||
declare -n ref=target
|
||||
[[ -R ref ]] && echo "ref is a nameref"
|
||||
[[ -R target ]] && echo "target is a nameref" || echo "target is not a nameref"
|
||||
declare +n ref
|
||||
[[ -R ref ]] && echo "still nameref" || echo "no longer a nameref"
|
||||
|
||||
- name: "Nameref -v follows nameref"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
target="value"
|
||||
declare -n ref=target
|
||||
[[ -v ref ]] && echo "ref is set (via target)"
|
||||
unset target
|
||||
[[ -v ref ]] && echo "ref still set" || echo "ref not set (target gone)"
|
||||
|
||||
- name: "Nameref -v to array element treats resolved name literally"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
arr=(a b c d)
|
||||
declare -n ref='arr[2]'
|
||||
[[ -v ref ]] && echo "set" || echo "unset"
|
||||
|
||||
- name: "Nameref -v with explicit subscript on nameref to whole array"
|
||||
# TODO(nameref): brush doesn't yet handle [[ -v "ref[N]" ]] where ref is
|
||||
# a nameref to a whole array. Bash resolves ref→arr and then checks arr[2],
|
||||
# but brush currently treats the subscript as part of the nameref target
|
||||
# string rather than applying it after resolution. The fix belongs in
|
||||
# extendedtests.rs (ShellVariableIsSetAndAssigned) — the operand "ref[2]"
|
||||
# needs to be split into name="ref" + subscript="2", the nameref resolved
|
||||
# on the name portion, then the subscript applied to the resolved target.
|
||||
known_failure: true
|
||||
stdin: |
|
||||
arr=(a b c d)
|
||||
declare -n ref2=arr
|
||||
[[ -v "ref2[2]" ]] && echo "set" || echo "unset"
|
||||
|
||||
- name: "Nameref -v to nonexistent array index"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
arr=(a b c)
|
||||
declare -n ref='arr[5]'
|
||||
[[ -v ref ]] && echo "set" || echo "unset"
|
||||
@@ -0,0 +1,177 @@
|
||||
name: "Functions"
|
||||
cases:
|
||||
- name: "Basic function invocation"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
myfunc() {
|
||||
echo "In myfunc()."
|
||||
}
|
||||
echo "Calling myfunc()..."
|
||||
myfunc
|
||||
echo "Returned."
|
||||
args: ["./script.sh"]
|
||||
|
||||
- name: "Function invocation with args"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
myfunc() {
|
||||
echo "In myfunc()"
|
||||
echo "1: $1"
|
||||
echo "*: $*"
|
||||
}
|
||||
echo "Calling myfunc()..."
|
||||
myfunc a b c
|
||||
echo "Returned."
|
||||
args: ["./script.sh"]
|
||||
|
||||
- name: "Function invocation with empty arg"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
echo "count: ${#*}"
|
||||
echo "\$1: $1"
|
||||
echo "\$2: $2"
|
||||
echo "\$3: $3"
|
||||
}
|
||||
|
||||
myfunc a b c
|
||||
myfunc a "" c
|
||||
|
||||
- name: "Function definition with output redirection"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
echo "In myfunc()"
|
||||
} >>./test.txt
|
||||
|
||||
myfunc
|
||||
myfunc
|
||||
|
||||
- name: "Function call with env variables"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
echo ${myvar}
|
||||
}
|
||||
|
||||
myvar="default"
|
||||
myfunc
|
||||
myvar="overridden" myfunc
|
||||
myfunc
|
||||
|
||||
- name: "Function definition without braces"
|
||||
stdin: |
|
||||
myfunc()
|
||||
if true; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
|
||||
myfunc
|
||||
|
||||
- name: "Nested function definition"
|
||||
stdin: |
|
||||
outer() {
|
||||
echo "Entered outer"
|
||||
|
||||
inner() {
|
||||
echo "In inner"
|
||||
}
|
||||
|
||||
echo "Invoking inner"
|
||||
|
||||
inner
|
||||
|
||||
echo "Returning from outer"
|
||||
}
|
||||
|
||||
echo "Calling outer from toplevel"
|
||||
outer
|
||||
|
||||
echo "Calling inner from toplevel"
|
||||
inner
|
||||
|
||||
- name: "Exporting functions to child instance"
|
||||
stdin: |
|
||||
mytestfunc() {
|
||||
echo "In mytestfunc"
|
||||
}
|
||||
|
||||
export -f mytestfunc
|
||||
|
||||
$0 -c 'mytestfunc'
|
||||
|
||||
- name: "Function names with interesting characters"
|
||||
stdin: |
|
||||
my/func() {
|
||||
echo "In my/func"
|
||||
}
|
||||
|
||||
my/func
|
||||
|
||||
- name: "Functions shadowing builtins"
|
||||
stdin: |
|
||||
.() {
|
||||
echo "In custom dot function"
|
||||
}
|
||||
|
||||
echo "Func result: $?"
|
||||
|
||||
.
|
||||
echo "Call result: $?"
|
||||
|
||||
- name: "Reserved words cannot be used as function names without function keyword"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
for (){ :; }
|
||||
echo "exit: $?"
|
||||
|
||||
- name: "Reserved words can be used as function names with function keyword"
|
||||
stdin: |
|
||||
function for { echo "custom for"; }
|
||||
for x in 1; do echo "normal for: $x"; done
|
||||
|
||||
- name: "Reserved words can be used as function names with function keyword and parens"
|
||||
stdin: |
|
||||
function while() { echo "custom while"; }
|
||||
while false; do echo "nope"; done
|
||||
echo "done"
|
||||
|
||||
- name: "Functions shadowing builtins (POSIX mode)"
|
||||
ignore_stderr: true
|
||||
stdin: |
|
||||
set -o posix
|
||||
|
||||
.() {
|
||||
echo "In custom dot function"
|
||||
}
|
||||
|
||||
echo "Func result: $?"
|
||||
|
||||
.
|
||||
echo "Call result: $?"
|
||||
|
||||
- name: "Empty string arguments preserved with nullglob"
|
||||
stdin: |
|
||||
shopt -s nullglob
|
||||
|
||||
myfunc() {
|
||||
echo "arg count: $#"
|
||||
for arg in "$@"; do
|
||||
echo "arg: '$arg'"
|
||||
done
|
||||
}
|
||||
|
||||
myfunc 3 - 2 "" 1.2.3b_alpha4
|
||||
|
||||
- name: "Multiple empty string arguments"
|
||||
stdin: |
|
||||
myfunc() {
|
||||
echo "count: $#"
|
||||
echo "1: '$1'"
|
||||
echo "2: '$2'"
|
||||
echo "3: '$3'"
|
||||
echo "4: '$4'"
|
||||
}
|
||||
|
||||
myfunc "" "non-empty" "" "last"
|
||||
@@ -0,0 +1,283 @@
|
||||
name: "Here docs/strings"
|
||||
cases:
|
||||
- name: "Basic here doc"
|
||||
stdin: |
|
||||
cat <<END-MARKER
|
||||
Something here...
|
||||
...and here.
|
||||
END-MARKER
|
||||
echo "This is after"
|
||||
|
||||
- name: "Basic here doc in a script"
|
||||
test_files:
|
||||
- path: "script.sh"
|
||||
contents: |
|
||||
cat <<END-MARKER
|
||||
Something here...
|
||||
...and here.
|
||||
END-MARKER
|
||||
echo "This is after"
|
||||
args: ["./script.sh"]
|
||||
|
||||
- name: "Here doc in a function"
|
||||
stdin: |
|
||||
function myfunc() {
|
||||
if true
|
||||
then
|
||||
cat << END-MARKER
|
||||
Something here...
|
||||
END-MARKER
|
||||
fi
|
||||
}
|
||||
|
||||
myfunc
|
||||
|
||||
- name: "Here doc with expansions"
|
||||
stdin: |
|
||||
cat <<END-MARKER
|
||||
Something here...
|
||||
...and here.
|
||||
$(echo "This is after")
|
||||
END-MARKER
|
||||
|
||||
- name: "Here doc with expansions but quoted tag"
|
||||
stdin: |
|
||||
cat <<"END-MARKER"
|
||||
Something here...
|
||||
...and here.
|
||||
$(echo "This is after")
|
||||
END-MARKER
|
||||
|
||||
- name: "Here doc with tab removal"
|
||||
stdin: |
|
||||
cat <<-END-MARKER
|
||||
Something here...
|
||||
...and here.
|
||||
END-MARKER
|
||||
|
||||
- name: "Basic here string"
|
||||
stdin: |
|
||||
shopt -ou posix
|
||||
cat <<<"Something here."
|
||||
wc -l <<<"Something"
|
||||
|
||||
- name: "Empty here doc"
|
||||
stdin: |
|
||||
wc -l <<EOF
|
||||
EOF
|
||||
|
||||
- name: "Basic here doc"
|
||||
stdin: "wc -l <<EOF\nSomething\nEOF"
|
||||
|
||||
- name: "Here doc with other tokens after tag"
|
||||
stdin: |
|
||||
wc -l <<EOF | wc -l
|
||||
A B C
|
||||
1 2 3
|
||||
EOF
|
||||
|
||||
- name: "Multiple here docs"
|
||||
stdin: |
|
||||
cat <<EOF1 <<EOF2
|
||||
A B C
|
||||
EOF1
|
||||
1 2 3
|
||||
EOF2
|
||||
|
||||
- name: "Here doc in a command substitution"
|
||||
stdin: |
|
||||
test1=$(cat <<EOF
|
||||
1
|
||||
2 3
|
||||
4 5 6
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "${test1}"
|
||||
|
||||
- name: "Here doc in a double-quoted command substitution"
|
||||
stdin: |
|
||||
test1="$(cat <<EOF
|
||||
something
|
||||
EOF
|
||||
)"
|
||||
|
||||
echo "${test1}"
|
||||
|
||||
- name: "Here doc in a double-quoted command substitution with spaces"
|
||||
stdin: |
|
||||
test1="$(cat << EOF
|
||||
something
|
||||
EOF
|
||||
)"
|
||||
|
||||
echo "${test1}"
|
||||
|
||||
- name: "Here doc in a command substitution with quotes"
|
||||
stdin: |
|
||||
test1=$(cat <<EOF
|
||||
something "quoted"
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "${test1}"
|
||||
|
||||
- name: "Here doc with parameter default containing quotes"
|
||||
stdin: |
|
||||
cat <<EOF
|
||||
${unset_var:-"default value"}
|
||||
EOF
|
||||
|
||||
- name: "Here doc preserves literal double quotes"
|
||||
stdin: |
|
||||
cat <<EOF
|
||||
"double quoted"
|
||||
she said "hello"
|
||||
EOF
|
||||
|
||||
- name: "Here doc preserves literal single quotes"
|
||||
stdin: |
|
||||
cat <<EOF
|
||||
'single quoted'
|
||||
it's a test
|
||||
EOF
|
||||
|
||||
- name: "Here doc with mixed quotes and expansion"
|
||||
stdin: |
|
||||
x=world
|
||||
cat <<EOF
|
||||
"hello $x"
|
||||
"$x" and more
|
||||
EOF
|
||||
|
||||
- name: "Here doc does not perform brace expansion"
|
||||
stdin: |
|
||||
cat <<EOF
|
||||
{a,b,c}
|
||||
{1..3}
|
||||
EOF
|
||||
|
||||
- name: "Here doc does not perform tilde expansion"
|
||||
stdin: |
|
||||
cat <<EOF
|
||||
~
|
||||
~/path
|
||||
EOF
|
||||
|
||||
- name: "Here doc escape sequences"
|
||||
stdin: |
|
||||
cat <<EOF
|
||||
\$HOME
|
||||
\\backslash
|
||||
\n not a newline
|
||||
\"not unquoted\"
|
||||
\'not unquoted\'
|
||||
EOF
|
||||
|
||||
- name: "Here doc with command substitution containing quotes"
|
||||
stdin: |
|
||||
cat <<EOF
|
||||
$(echo "hello world")
|
||||
$(echo 'single')
|
||||
EOF
|
||||
|
||||
- name: "Here doc with arithmetic expansion"
|
||||
stdin: |
|
||||
cat <<EOF
|
||||
$((2 + 3))
|
||||
$((10 * 2))
|
||||
EOF
|
||||
|
||||
- name: "Here doc in a command substitution with parentheses"
|
||||
stdin: |
|
||||
test1=$(cat <<EOF
|
||||
(something)
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "${test1}"
|
||||
|
||||
- name: "Here doc with parentheses"
|
||||
stdin: |
|
||||
cat <<EOF
|
||||
(something)
|
||||
EOF
|
||||
|
||||
- name: "Multiple here docs in a command substitution"
|
||||
stdin: |
|
||||
test1=$(cat <<EOF1 <<EOF2
|
||||
A B C
|
||||
EOF1
|
||||
D E F
|
||||
EOF2
|
||||
)
|
||||
|
||||
echo "${test1}"
|
||||
|
||||
- name: "Complex here docs in a command substitution"
|
||||
stdin: |
|
||||
test1=$(cat <<EOF1 <<EOF2 | wc -l
|
||||
A B C
|
||||
EOF1
|
||||
D E F
|
||||
EOF2
|
||||
)
|
||||
|
||||
echo "${test1}"
|
||||
|
||||
- name: "Here doc with quoted delimiter"
|
||||
stdin: |
|
||||
cat <<'EOF'
|
||||
something
|
||||
EOF
|
||||
|
||||
- name: "Here doc with double quoted delimiter"
|
||||
stdin: |
|
||||
cat <<"EOF"
|
||||
something
|
||||
EOF
|
||||
|
||||
- name: "Here doc with quoted delimiter inside command substitution"
|
||||
stdin: |
|
||||
$(cat <<'EOF'
|
||||
true
|
||||
EOF
|
||||
)
|
||||
|
||||
- name: "Here doc with double quoted delimiter inside command substitution"
|
||||
stdin: |
|
||||
$(cat <<"EOF"
|
||||
true
|
||||
EOF
|
||||
)
|
||||
|
||||
- name: "Here doc with unpaired single quote in command substitution"
|
||||
stdin: |
|
||||
echo "$(cat <<'EOF'
|
||||
it's here
|
||||
EOF
|
||||
)"
|
||||
|
||||
- name: "Here doc with unpaired backtick in command substitution"
|
||||
stdin: |
|
||||
echo "$(cat <<'EOF'
|
||||
a ` b
|
||||
EOF
|
||||
)"
|
||||
|
||||
- name: "Here doc with three single quotes in command substitution"
|
||||
stdin: |
|
||||
echo "$(cat <<'EOF'
|
||||
it's Bob's it's
|
||||
EOF
|
||||
)"
|
||||
|
||||
- name: "Here doc with unpaired single quote in command substitution (#420)"
|
||||
stdin: |
|
||||
var=$(
|
||||
sed 's/nice/cool/' - <<-EOF
|
||||
that's
|
||||
"nice"
|
||||
EOF
|
||||
)
|
||||
echo $var
|
||||
@@ -0,0 +1,405 @@
|
||||
name: "IFS"
|
||||
cases:
|
||||
- name: "Default IFS value"
|
||||
stdin: |
|
||||
# Default IFS is space, tab, newline
|
||||
[[ "$IFS" == $' \t\n' ]] && echo "IFS has default value"
|
||||
|
||||
- name: "Field splitting with default IFS"
|
||||
stdin: |
|
||||
var="one two three
|
||||
four"
|
||||
echo "Fields:" $var
|
||||
|
||||
- name: "Field splitting with custom single-char IFS"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
var="one:two:three"
|
||||
echo $var
|
||||
|
||||
- name: "Field splitting with multi-char IFS"
|
||||
stdin: |
|
||||
IFS=':,'
|
||||
var="one:two,three:four"
|
||||
echo $var
|
||||
|
||||
- name: "Empty IFS disables field splitting"
|
||||
stdin: |
|
||||
IFS=''
|
||||
var="one two three"
|
||||
echo "[$var]"
|
||||
# Should print with all whitespace preserved
|
||||
|
||||
- name: "Empty IFS with expansion"
|
||||
stdin: |
|
||||
IFS=''
|
||||
var="a b c"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "[$1]"
|
||||
|
||||
- name: "Unset IFS uses default splitting"
|
||||
stdin: |
|
||||
unset IFS
|
||||
var="one two three"
|
||||
echo $var
|
||||
|
||||
- name: "IFS whitespace vs non-whitespace splitting"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
# Non-whitespace IFS chars create empty fields
|
||||
IFS=':'
|
||||
var=":one::two:"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "Args: $@"
|
||||
|
||||
- name: "IFS whitespace splitting ignores leading/trailing"
|
||||
stdin: |
|
||||
# Whitespace IFS chars ignore leading/trailing
|
||||
IFS=' '
|
||||
var=" one two "
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "Args: $@"
|
||||
|
||||
- name: "Mixed whitespace and non-whitespace IFS"
|
||||
stdin: |
|
||||
IFS=' :'
|
||||
var="one:two three : four"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
for arg in "$@"; do
|
||||
echo "[$arg]"
|
||||
done
|
||||
|
||||
- name: "Mixed IFS leading non-whitespace"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
IFS=': '
|
||||
var=":one: :two"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
for arg in "$@"; do
|
||||
echo "[$arg]"
|
||||
done
|
||||
|
||||
- name: "Mixed IFS trailing non-whitespace"
|
||||
stdin: |
|
||||
IFS=' :'
|
||||
var="one :two: "
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
for arg in "$@"; do
|
||||
echo "[$arg]"
|
||||
done
|
||||
|
||||
- name: "IFS only whitespace multiple chars"
|
||||
stdin: |
|
||||
IFS=$' \t'
|
||||
var=" one two three "
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "Args: $@"
|
||||
|
||||
- name: "IFS only non-whitespace multiple chars"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
IFS=':,'
|
||||
var=":,one,:,two,:"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
for arg in "$@"; do
|
||||
echo "[$arg]"
|
||||
done
|
||||
|
||||
- name: "IFS only tab character"
|
||||
stdin: |
|
||||
IFS=$'\t'
|
||||
var=" a b "
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "Args: $@"
|
||||
|
||||
- name: "IFS only newline character"
|
||||
stdin: |
|
||||
IFS=$'\n'
|
||||
var="
|
||||
|
||||
a
|
||||
|
||||
b
|
||||
|
||||
"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "Args: $@"
|
||||
|
||||
- name: "IFS affects command substitution"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
result=$(echo "a:b:c")
|
||||
# Command substitution itself doesn't split, but expansion does
|
||||
set -- $result
|
||||
echo "$#"
|
||||
echo "$@"
|
||||
|
||||
- name: "IFS with command substitution multiline"
|
||||
stdin: |
|
||||
IFS=$'\n'
|
||||
result=$(printf "a\nb\nc")
|
||||
set -- $result
|
||||
echo "Count: $#"
|
||||
echo "$1|$2|$3"
|
||||
|
||||
- name: "IFS affects arithmetic expansion"
|
||||
stdin: |
|
||||
IFS=' '
|
||||
# Arithmetic expansion result is splittable
|
||||
set -- $((1 + 1)) $((2 + 2))
|
||||
echo "Count: $#"
|
||||
echo "$@"
|
||||
|
||||
- name: "IFS in array expansion"
|
||||
stdin: |
|
||||
arr=(one two three)
|
||||
IFS=':'
|
||||
echo "${arr[*]}"
|
||||
# With $* should join with first char of IFS
|
||||
|
||||
- name: "IFS with $* vs $@"
|
||||
stdin: |
|
||||
set -- one two three
|
||||
IFS=':'
|
||||
echo "$*"
|
||||
echo "$@"
|
||||
|
||||
- name: "IFS with $* unquoted"
|
||||
stdin: |
|
||||
set -- "a:b" "c:d"
|
||||
IFS=':'
|
||||
# Unquoted $* joins then splits
|
||||
set -- $*
|
||||
echo "Count: $#"
|
||||
for arg in "$@"; do
|
||||
echo "[$arg]"
|
||||
done
|
||||
|
||||
- name: "IFS null char handling"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
var=""
|
||||
set -- $var
|
||||
echo "Empty var count: $#"
|
||||
|
||||
- name: "IFS preservation across commands"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
echo "First: $IFS"
|
||||
var="a:b"
|
||||
echo $var
|
||||
echo "After: $IFS"
|
||||
|
||||
- name: "IFS in subshell"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
(
|
||||
echo "In subshell: [$IFS]"
|
||||
var="a:b:c"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
)
|
||||
echo "Parent: [$IFS]"
|
||||
|
||||
- name: "IFS local to command"
|
||||
stdin: |
|
||||
var="a:b:c"
|
||||
IFS=':' echo $var
|
||||
# IFS assignment should only affect that command
|
||||
echo "After: [$IFS]"
|
||||
|
||||
- name: "IFS does not affect for loop literal words"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
IFS=':'
|
||||
for word in a:b:c; do
|
||||
echo "[$word]"
|
||||
done
|
||||
|
||||
- name: "IFS affects for loop variable expansion"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
var="a:b:c"
|
||||
for word in $var; do
|
||||
echo "[$word]"
|
||||
done
|
||||
|
||||
- name: "IFS affects for loop command substitution"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
for word in $(echo "x:y:z"); do
|
||||
echo "[$word]"
|
||||
done
|
||||
|
||||
- name: "IFS with quoted vs unquoted expansion"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
var="a:b:c"
|
||||
echo "Unquoted:" $var
|
||||
echo "Quoted: $var"
|
||||
|
||||
- name: "IFS with partially quoted expansion"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
IFS=':'
|
||||
var="a:b:c"
|
||||
set -- "$var":more
|
||||
echo "Count: $#"
|
||||
echo "$@"
|
||||
|
||||
- name: "IFS whitespace sequence handling"
|
||||
stdin: |
|
||||
IFS=' '
|
||||
var="a b"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "Args: $@"
|
||||
|
||||
- name: "IFS non-whitespace sequence handling"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
IFS=':'
|
||||
var="a::::b"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
for arg in "$@"; do
|
||||
echo "[$arg]"
|
||||
done
|
||||
|
||||
- name: "IFS with parameter expansion"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
var="a:b:c"
|
||||
result=${var}
|
||||
set -- $result
|
||||
echo "$#"
|
||||
echo "$@"
|
||||
|
||||
- name: "IFS with parameter expansion default value"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
set -- ${unset:-a:b:c}
|
||||
echo "Count: $#"
|
||||
echo "$@"
|
||||
|
||||
- name: "IFS with parameter expansion empty default"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
set -- ${unset:-}
|
||||
echo "Count: $#"
|
||||
|
||||
- name: "IFS with parameter length"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
var="a:b:c"
|
||||
# Length expansion returns a number, not subject to further splitting
|
||||
len=${#var}
|
||||
set -- $len
|
||||
echo "Count: $#"
|
||||
echo "[$1]"
|
||||
|
||||
- name: "IFS with glob pattern literal"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
IFS=':'
|
||||
# Glob patterns as literals shouldn't split
|
||||
for word in *.txt:*.sh; do
|
||||
echo "[$word]"
|
||||
done
|
||||
|
||||
- name: "IFS newline handling"
|
||||
stdin: |
|
||||
IFS=$'\n'
|
||||
var="a
|
||||
b
|
||||
c"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "$1|$2|$3"
|
||||
|
||||
- name: "IFS tab handling"
|
||||
stdin: |
|
||||
IFS=$'\t'
|
||||
var="a b c"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "$1|$2|$3"
|
||||
|
||||
- name: "IFS with backslash in value"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
var='a\:b:c\:d'
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
for arg in "$@"; do
|
||||
echo "[$arg]"
|
||||
done
|
||||
|
||||
- name: "IFS first char for joining"
|
||||
stdin: |
|
||||
set -- a b c
|
||||
IFS=':'
|
||||
# First char of IFS used for $* joining
|
||||
result="$*"
|
||||
echo "[$result]"
|
||||
|
||||
- name: "IFS first char empty IFS"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
set -- a b c
|
||||
IFS=''
|
||||
result="$*"
|
||||
echo "[$result]"
|
||||
|
||||
- name: "IFS multiple whitespace at start"
|
||||
stdin: |
|
||||
IFS=' '
|
||||
var=" a b"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "$@"
|
||||
|
||||
- name: "IFS multiple whitespace at end"
|
||||
stdin: |
|
||||
IFS=' '
|
||||
var="a b "
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
echo "$@"
|
||||
|
||||
- name: "IFS with read builtin integration"
|
||||
stdin: |
|
||||
IFS=':'
|
||||
echo "a:b:c" | (read x y z; echo "x=[$x] y=[$y] z=[$z]")
|
||||
|
||||
- name: "IFS whitespace before non-whitespace"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
IFS=' :'
|
||||
var=" :a"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
for arg in "$@"; do
|
||||
echo "[$arg]"
|
||||
done
|
||||
|
||||
- name: "IFS non-whitespace before whitespace"
|
||||
known_failure: true
|
||||
stdin: |
|
||||
IFS=': '
|
||||
var=": a"
|
||||
set -- $var
|
||||
echo "Count: $#"
|
||||
for arg in "$@"; do
|
||||
echo "[$arg]"
|
||||
done
|
||||
@@ -0,0 +1,48 @@
|
||||
name: "Interactive"
|
||||
incompatible_configs: ["sh"]
|
||||
cases:
|
||||
- name: "Basic interactive test"
|
||||
pty: true
|
||||
ignore_stdout: true # Needs investigation
|
||||
ignore_whitespace: true # There's an extra newline for now in test output
|
||||
stdin: |
|
||||
#expect-prompt
|
||||
echo hi
|
||||
#send:Enter
|
||||
#expect:hi
|
||||
#expect-prompt
|
||||
#send:Ctrl+D
|
||||
|
||||
- name: "Simple tab completion"
|
||||
skip: true # TODO(input): Need to find the right way to enable this with a rich input backend.
|
||||
pty: true
|
||||
ignore_stdout: true
|
||||
test_files:
|
||||
- path: "test-file.txt"
|
||||
contents: |
|
||||
Hello, world.
|
||||
stdin: |
|
||||
#expect-prompt
|
||||
cat test-file.
|
||||
#send:Tab
|
||||
#send:Enter
|
||||
#expect:Hello, world.
|
||||
#expect-prompt
|
||||
#send:Ctrl+D
|
||||
|
||||
- name: "PROMPT_COMMAND"
|
||||
ignore_stderr: true
|
||||
args: ["-i"]
|
||||
stdin: |
|
||||
declare -a calls=()
|
||||
command=0
|
||||
|
||||
PROMPT_COMMAND='calls+=("$command")'
|
||||
|
||||
command=first
|
||||
command=second
|
||||
command=third
|
||||
|
||||
unset PROMPT_COMMAND
|
||||
|
||||
declare -p calls
|
||||
@@ -0,0 +1,29 @@
|
||||
name: "List"
|
||||
cases:
|
||||
- name: "Ignore single quote in comment in list"
|
||||
stdin: |
|
||||
(
|
||||
# I'm
|
||||
echo "Batman"
|
||||
)
|
||||
|
||||
- name: "Ignore double quote in comment in list"
|
||||
stdin: |
|
||||
(
|
||||
# This " is not being
|
||||
echo "parsed"
|
||||
)
|
||||
|
||||
- name: "Ignore parentheses in comment in list"
|
||||
stdin: |
|
||||
(
|
||||
# :(
|
||||
echo "Sad"
|
||||
)
|
||||
|
||||
- name: "Ignore dollar in comment in list"
|
||||
stdin: |
|
||||
(
|
||||
# $
|
||||
echo "Mr. Crabs ^"
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
name: "BASH_XTRACEFD tests"
|
||||
cases:
|
||||
- name: "basic redirection to fd 3"
|
||||
stdin: |
|
||||
exec 3>trace.txt
|
||||
BASH_XTRACEFD=3
|
||||
|
||||
set -x
|
||||
echo "hello"
|
||||
echo "world"
|
||||
|
||||
set +x
|
||||
cat trace.txt
|
||||
|
||||
- name: "redirection takes effect immediately"
|
||||
stdin: |
|
||||
exec 3>trace.txt
|
||||
|
||||
set -x
|
||||
echo "before setting BASH_XTRACEFD"
|
||||
|
||||
BASH_XTRACEFD=3
|
||||
echo "after setting BASH_XTRACEFD"
|
||||
|
||||
set +x
|
||||
|
||||
echo "Content of trace file:"
|
||||
cat trace.txt
|
||||
|
||||
- name: "unsetting BASH_XTRACEFD returns to stderr"
|
||||
stdin: |
|
||||
exec 3>trace.txt
|
||||
BASH_XTRACEFD=3
|
||||
|
||||
set -x
|
||||
echo "redirected"
|
||||
|
||||
unset BASH_XTRACEFD
|
||||
echo "back to stderr"
|
||||
|
||||
set +x
|
||||
|
||||
echo "Content of trace file:"
|
||||
cat trace.txt
|
||||
|
||||
- name: "invalid fd value falls back to stderr"
|
||||
known_failure: true # bash validates BASH_XTRACEFD at assignment time, brush doesn't have that mechanism
|
||||
stdin: |
|
||||
BASH_XTRACEFD=999
|
||||
|
||||
set -x
|
||||
echo "should go to stderr"
|
||||
|
||||
set +x
|
||||
|
||||
- name: "non-numeric value falls back to stderr"
|
||||
known_failure: true # bash validates BASH_XTRACEFD at assignment time, brush doesn't have that mechanism
|
||||
stdin: |
|
||||
BASH_XTRACEFD=invalid
|
||||
|
||||
set -x
|
||||
echo "should go to stderr"
|
||||
|
||||
set +x
|
||||
|
||||
- name: "empty value falls back to stderr"
|
||||
stdin: |
|
||||
BASH_XTRACEFD=
|
||||
|
||||
set -x
|
||||
echo "should go to stderr"
|
||||
|
||||
- name: "redirect to file that fails writes"
|
||||
stdin: |
|
||||
# This test relies on /dev/full being present. We degrade gracefully
|
||||
# if it doesn't.
|
||||
[[ -e /dev/full ]] || exit
|
||||
|
||||
exec 3>/dev/full
|
||||
BASH_XTRACEFD=3
|
||||
|
||||
set -x
|
||||
echo "first echo"
|
||||
echo "first echo result: $?"
|
||||
@@ -0,0 +1,8 @@
|
||||
name: "Default options"
|
||||
cases:
|
||||
- name: "Default options"
|
||||
stdin: |
|
||||
echo "Default options: $-"
|
||||
|
||||
- name: "Default options (-c)"
|
||||
args: ["-c", 'echo "Default options: $-"']
|
||||
@@ -0,0 +1,213 @@
|
||||
name: "Options: errtrace (-E)"
|
||||
cases:
|
||||
#
|
||||
# Basic errtrace option behavior
|
||||
#
|
||||
- name: "errtrace disabled by default"
|
||||
stdin: |
|
||||
set +E 2>/dev/null || true
|
||||
echo "errtrace test"
|
||||
|
||||
- name: "errtrace can be enabled and disabled"
|
||||
stdin: |
|
||||
set -E
|
||||
echo "enabled: $-" | grep -q E && echo "E is set"
|
||||
set +E
|
||||
echo "disabled: $-" | grep -qv E && echo "E is unset"
|
||||
|
||||
#
|
||||
# ERR trap inheritance in functions (core errtrace behavior)
|
||||
#
|
||||
- name: "errtrace disabled - functions don't inherit ERR trap"
|
||||
stdin: |
|
||||
set +E
|
||||
trap 'echo "ERR trapped"' ERR
|
||||
myfunc() { false; }
|
||||
myfunc
|
||||
echo "after"
|
||||
|
||||
- name: "errtrace enabled - functions inherit ERR trap"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "ERR trapped"' ERR
|
||||
myfunc() { false; }
|
||||
myfunc
|
||||
echo "after"
|
||||
|
||||
- name: "errtrace with nested functions"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "ERR trapped"' ERR
|
||||
inner() { false; }
|
||||
outer() { inner; }
|
||||
outer
|
||||
echo "after"
|
||||
|
||||
- name: "errtrace toggling mid-script"
|
||||
stdin: |
|
||||
trap 'echo "ERR trapped"' ERR
|
||||
set -E
|
||||
myfunc1() { false; }
|
||||
myfunc1
|
||||
echo "after first call"
|
||||
set +E
|
||||
myfunc2() { false; }
|
||||
myfunc2
|
||||
echo "after second call"
|
||||
|
||||
#
|
||||
# ERR trap inheritance in subshells
|
||||
#
|
||||
- name: "errtrace disabled - subshell doesn't inherit ERR trap"
|
||||
stdin: |
|
||||
set +E
|
||||
trap 'echo "ERR"' ERR
|
||||
(
|
||||
trap -p ERR
|
||||
false
|
||||
echo "no ERR trap fired"
|
||||
)
|
||||
echo "after"
|
||||
|
||||
- name: "errtrace enabled - subshell inherits ERR trap"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "ERR"' ERR
|
||||
(
|
||||
trap -p ERR | grep -q ERR && echo "ERR trap inherited"
|
||||
false
|
||||
echo "after false in subshell"
|
||||
)
|
||||
echo "after"
|
||||
|
||||
#
|
||||
# ERR trap inheritance in command substitution
|
||||
#
|
||||
- name: "errtrace disabled - command substitution doesn't inherit ERR trap"
|
||||
stdin: |
|
||||
set +E
|
||||
trap 'echo "[ERR trapped]" >&2' ERR
|
||||
result=$(trap -p ERR; false; echo "no trap inherited")
|
||||
echo "result: $result"
|
||||
|
||||
- name: "errtrace enabled - command substitution inherits ERR trap"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "[ERR in subshell]" >&2' ERR
|
||||
result=$(false; echo "after false in subshell")
|
||||
echo "result: $result"
|
||||
|
||||
#
|
||||
# ERR trap inheritance in pipelines
|
||||
#
|
||||
- name: "errtrace with pipeline - last command fails"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "[ERR: $?]" >&2' ERR
|
||||
echo "data" | cat | false
|
||||
echo "after pipeline"
|
||||
|
||||
- name: "errtrace with pipeline - middle command fails (pipefail)"
|
||||
stdin: |
|
||||
set -E
|
||||
set -o pipefail
|
||||
trap 'echo "[ERR: $?]" >&2' ERR
|
||||
echo "data" | false | cat
|
||||
echo "after pipeline"
|
||||
|
||||
- name: "errtrace - ERR trap in pipeline subshells"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "[ERR: $BASH_COMMAND]" >&2' ERR
|
||||
echo "input" | { false; echo "after false in brace"; }
|
||||
echo "done"
|
||||
|
||||
#
|
||||
# ERR trap with errexit interaction
|
||||
#
|
||||
- name: "errtrace with errexit - ERR trap fires before exit"
|
||||
stdin: |
|
||||
set -e
|
||||
set -E
|
||||
trap 'echo "ERR trap fired"' ERR
|
||||
false
|
||||
echo "should not print"
|
||||
|
||||
- name: "errtrace with errexit in function"
|
||||
known_failure: true # TODO(traps): ERR trap not firing in function with errexit
|
||||
stdin: |
|
||||
set -e
|
||||
set -E
|
||||
trap 'echo "ERR trap"' ERR
|
||||
myfunc() { false; }
|
||||
myfunc
|
||||
echo "should not print"
|
||||
|
||||
#
|
||||
# ERR trap inheritance in deeply nested functions
|
||||
#
|
||||
- name: "errtrace - ERR trap in deeply nested function"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "ERR at level ${#FUNCNAME[@]}"' ERR
|
||||
level3() { false; }
|
||||
level2() { level3; }
|
||||
level1() { level2; }
|
||||
level1
|
||||
echo "after"
|
||||
|
||||
#
|
||||
# ERR trap modification within functions
|
||||
#
|
||||
- name: "errtrace - ERR trap can be changed inside function"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "outer ERR"' ERR
|
||||
myfunc() {
|
||||
trap 'echo "inner ERR"' ERR
|
||||
false
|
||||
}
|
||||
myfunc
|
||||
false
|
||||
echo "after"
|
||||
|
||||
- name: "errtrace - ERR trap can be cleared inside function"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "ERR"' ERR
|
||||
myfunc() {
|
||||
trap - ERR
|
||||
false
|
||||
echo "no ERR in func"
|
||||
}
|
||||
myfunc
|
||||
false
|
||||
echo "after"
|
||||
|
||||
#
|
||||
# ERR trap in background jobs with errtrace
|
||||
#
|
||||
- name: "errtrace - ERR trap in background job"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "ERR" >&2' ERR
|
||||
{ false; echo "bg done"; } &
|
||||
wait
|
||||
echo "main done"
|
||||
|
||||
#
|
||||
# Sourced scripts with errtrace
|
||||
#
|
||||
- name: "errtrace - ERR trap inherited in sourced script"
|
||||
test_files:
|
||||
- path: "failing.sh"
|
||||
contents: |
|
||||
echo "in sourced"
|
||||
false
|
||||
echo "after false in sourced"
|
||||
stdin: |
|
||||
set -E
|
||||
trap 'echo "ERR trapped"' ERR
|
||||
. ./failing.sh
|
||||
echo "after source"
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
name: "Options: extdebug"
|
||||
cases:
|
||||
#
|
||||
# extdebug implies functrace (-T)
|
||||
#
|
||||
- name: "extdebug implies functrace - DEBUG inherited in functions"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
myfunc() {
|
||||
echo "in func"
|
||||
}
|
||||
echo "before"
|
||||
myfunc
|
||||
echo "after"
|
||||
|
||||
- name: "extdebug implies functrace - DEBUG inherited in subshells"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
echo "before"
|
||||
(echo "in subshell")
|
||||
echo "after"
|
||||
|
||||
#
|
||||
# DEBUG trap return value effects (extdebug-specific)
|
||||
#
|
||||
- name: "DEBUG trap - returns 0 command executes normally"
|
||||
known_failure: true # TODO(extdebug): return in trap handler not supported
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap 'return 0' DEBUG
|
||||
echo "line 1"
|
||||
echo "line 2"
|
||||
echo "line 3"
|
||||
|
||||
- name: "DEBUG trap - returns 1 command is skipped"
|
||||
known_failure: true # TODO(extdebug): DEBUG trap return value not implemented
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
skip_next=false
|
||||
trap '
|
||||
if $skip_next; then
|
||||
skip_next=false
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
' DEBUG
|
||||
echo "line 1"
|
||||
skip_next=true
|
||||
echo "this should be skipped"
|
||||
echo "line 3"
|
||||
|
||||
- name: "DEBUG trap - returns 2 simulates return from function"
|
||||
known_failure: true # TODO(extdebug): DEBUG trap return value not implemented
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap '
|
||||
if [[ "${FUNCNAME[0]}" == "myfunc" && "$BASH_COMMAND" == *"should not"* ]]; then
|
||||
return 2
|
||||
fi
|
||||
return 0
|
||||
' DEBUG
|
||||
myfunc() {
|
||||
echo "start of func"
|
||||
echo "this should not print"
|
||||
echo "also should not print"
|
||||
}
|
||||
myfunc
|
||||
echo "after func: $?"
|
||||
|
||||
- name: "DEBUG trap - return value ignored without extdebug"
|
||||
known_failure: true # TODO(extdebug): return in trap handler error message differs
|
||||
stdin: |
|
||||
shopt -u extdebug
|
||||
trap 'return 1' DEBUG
|
||||
echo "line 1"
|
||||
echo "line 2"
|
||||
echo "line 3"
|
||||
|
||||
#
|
||||
# Command skipping scenarios (extdebug-specific)
|
||||
#
|
||||
- name: "Skip simple command"
|
||||
known_failure: true # TODO(extdebug): DEBUG trap return value not implemented
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap '[[ "$BASH_COMMAND" == *"skip"* ]] && return 1; return 0' DEBUG
|
||||
echo "keep this"
|
||||
echo "skip this"
|
||||
echo "keep this too"
|
||||
|
||||
- name: "Skip in loop - iteration skipped but loop continues"
|
||||
known_failure: true # TODO(extdebug): DEBUG trap return value not implemented
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap '[[ "$BASH_COMMAND" == *"skip"* ]] && return 1; return 0' DEBUG
|
||||
for i in 1 2 3; do
|
||||
echo "iter $i"
|
||||
echo "skip $i"
|
||||
echo "after skip $i"
|
||||
done
|
||||
echo "done"
|
||||
|
||||
- name: "Skip in conditional"
|
||||
known_failure: true # TODO(extdebug): DEBUG trap return value not implemented
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap '[[ "$BASH_COMMAND" == *"skip"* ]] && return 1; return 0' DEBUG
|
||||
if true; then
|
||||
echo "in then"
|
||||
echo "skip in then"
|
||||
echo "after skip in then"
|
||||
fi
|
||||
echo "after if"
|
||||
|
||||
- name: "Return 2 in nested function - unwinds to caller"
|
||||
known_failure: true # TODO(extdebug): DEBUG trap return value not implemented
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap '
|
||||
if [[ "${FUNCNAME[0]}" == "inner" && "$BASH_COMMAND" == *"inner cmd"* ]]; then
|
||||
return 2
|
||||
fi
|
||||
return 0
|
||||
' DEBUG
|
||||
inner() {
|
||||
echo "inner start"
|
||||
echo "inner cmd"
|
||||
echo "inner end"
|
||||
}
|
||||
outer() {
|
||||
echo "outer start"
|
||||
inner
|
||||
echo "outer after inner"
|
||||
}
|
||||
outer
|
||||
echo "main after outer"
|
||||
|
||||
#
|
||||
# BASH_ARGC and BASH_ARGV (extdebug-specific)
|
||||
#
|
||||
- name: "BASH_ARGC - contains argument counts per call frame"
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
show_argc() {
|
||||
echo "BASH_ARGC: ${BASH_ARGC[*]}"
|
||||
}
|
||||
func_with_args() {
|
||||
show_argc
|
||||
}
|
||||
func_with_args a b c
|
||||
|
||||
- name: "BASH_ARGV - contains arguments in reverse order"
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
show_argv() {
|
||||
echo "BASH_ARGV: ${BASH_ARGV[*]}"
|
||||
}
|
||||
func_with_args() {
|
||||
show_argv
|
||||
}
|
||||
func_with_args a b c
|
||||
|
||||
- name: "BASH_ARGC/ARGV - in nested calls"
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
inner() {
|
||||
echo "inner ARGC: ${BASH_ARGC[*]}"
|
||||
echo "inner ARGV: ${BASH_ARGV[*]}"
|
||||
}
|
||||
outer() {
|
||||
inner x y
|
||||
}
|
||||
outer a b c
|
||||
|
||||
- name: "BASH_ARGC/ARGV - empty when no arguments"
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
no_args() {
|
||||
echo "BASH_ARGC: '${BASH_ARGC[*]}'"
|
||||
echo "BASH_ARGV: '${BASH_ARGV[*]}'"
|
||||
}
|
||||
no_args
|
||||
|
||||
#
|
||||
# declare -F shows source info (extdebug-specific)
|
||||
#
|
||||
- name: "declare -F - shows function name only without extdebug"
|
||||
stdin: |
|
||||
myfunc() { echo hi; }
|
||||
declare -F myfunc
|
||||
|
||||
- name: "declare -F - shows filename and line number with extdebug"
|
||||
known_failure: true # TODO(extdebug): declare -F not showing source info
|
||||
test_files:
|
||||
- path: "funcs.sh"
|
||||
contents: |
|
||||
myfunc() {
|
||||
echo hi
|
||||
}
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
. ./funcs.sh
|
||||
declare -F myfunc | sed 's/[0-9]*/N/'
|
||||
|
||||
#
|
||||
# Interaction with other options
|
||||
#
|
||||
- name: "extdebug with errexit - skipped command doesn't trigger errexit"
|
||||
known_failure: true # TODO(extdebug): DEBUG trap return value not implemented
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
set -e
|
||||
trap '[[ "$BASH_COMMAND" == "false" ]] && return 1; return 0' DEBUG
|
||||
echo "before"
|
||||
false
|
||||
echo "after - should print because false was skipped"
|
||||
|
||||
- name: "extdebug disabled after being enabled"
|
||||
known_failure: true # TODO(extdebug): DEBUG trap return value not implemented
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap 'return 1' DEBUG
|
||||
echo "line 1 - should be skipped"
|
||||
shopt -u extdebug
|
||||
echo "line 2 - should print"
|
||||
echo "line 3 - should print"
|
||||
|
||||
- name: "extdebug with set -T - both affect DEBUG inheritance"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
set -T
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
myfunc() {
|
||||
echo "in func"
|
||||
}
|
||||
myfunc
|
||||
echo "done"
|
||||
|
||||
#
|
||||
# DEBUG trap with extdebug in various contexts
|
||||
#
|
||||
- name: "DEBUG trap with extdebug in function"
|
||||
known_failure: true # TODO(extdebug): return in trap handler not supported
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
count=0
|
||||
trap '
|
||||
((count++))
|
||||
echo "debug $count: $BASH_COMMAND"
|
||||
return 0
|
||||
' DEBUG
|
||||
myfunc() {
|
||||
local x=1
|
||||
echo "x=$x"
|
||||
}
|
||||
myfunc
|
||||
|
||||
- name: "DEBUG trap with extdebug controls command execution"
|
||||
known_failure: true # TODO(extdebug): DEBUG trap return value not implemented
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
should_skip=false
|
||||
trap '
|
||||
if $should_skip; then
|
||||
should_skip=false
|
||||
echo "[skipping: $BASH_COMMAND]"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
' DEBUG
|
||||
echo "first"
|
||||
should_skip=true
|
||||
echo "second - will be skipped"
|
||||
echo "third"
|
||||
|
||||
#
|
||||
# Edge cases
|
||||
#
|
||||
- name: "extdebug with recursive function"
|
||||
known_failure: true # TODO(extdebug): DEBUG trap firing count differs
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap 'echo "depth: ${#FUNCNAME[@]}"' DEBUG
|
||||
recurse() {
|
||||
local n=$1
|
||||
if ((n > 0)); then
|
||||
recurse $((n - 1))
|
||||
fi
|
||||
}
|
||||
recurse 3
|
||||
|
||||
- name: "BASH_COMMAND shows compound command"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
trap 'echo "cmd: $BASH_COMMAND"' DEBUG
|
||||
if true; then echo "in if"; fi
|
||||
|
||||
- name: "DEBUG trap can modify variables"
|
||||
known_failure: true # TODO(extdebug): return in trap handler not supported
|
||||
stdin: |
|
||||
shopt -s extdebug
|
||||
counter=0
|
||||
trap '((counter++)); return 0' DEBUG
|
||||
echo "a"
|
||||
echo "b"
|
||||
echo "c"
|
||||
echo "counter: $counter"
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
name: "Options: functrace (-T)"
|
||||
cases:
|
||||
#
|
||||
# Basic functrace option behavior
|
||||
#
|
||||
- name: "functrace can be enabled and disabled"
|
||||
stdin: |
|
||||
set -T
|
||||
echo "enabled: $-" | grep -q T && echo "T is set"
|
||||
set +T
|
||||
echo "disabled: $-" | grep -qv T && echo "T is unset"
|
||||
|
||||
#
|
||||
# DEBUG trap inheritance in functions
|
||||
#
|
||||
- name: "DEBUG trap - does not fire in function without set -T"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
myfunc() {
|
||||
echo "in func"
|
||||
}
|
||||
echo "before func"
|
||||
myfunc
|
||||
echo "after func"
|
||||
|
||||
- name: "DEBUG trap - fires in function with set -T"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
myfunc() {
|
||||
echo "in func"
|
||||
}
|
||||
echo "before func"
|
||||
myfunc
|
||||
echo "after func"
|
||||
|
||||
#
|
||||
# DEBUG trap inheritance in subshells
|
||||
#
|
||||
- name: "DEBUG trap - does not fire in subshell without set -T"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
echo "before subshell"
|
||||
(echo "in subshell")
|
||||
echo "after subshell"
|
||||
|
||||
- name: "DEBUG trap - fires in subshell with set -T"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
echo "before subshell"
|
||||
(echo "in subshell")
|
||||
echo "after subshell"
|
||||
|
||||
#
|
||||
# DEBUG trap inheritance in command substitution
|
||||
#
|
||||
- name: "DEBUG trap - does not fire in command substitution without set -T"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
trap 'echo "[debug: $BASH_COMMAND]" >&2' DEBUG
|
||||
echo "before"
|
||||
result=$(echo "in substitution")
|
||||
echo "result: $result"
|
||||
|
||||
- name: "DEBUG trap - fires in command substitution with set -T"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[debug: $BASH_COMMAND]" >&2' DEBUG
|
||||
echo "before"
|
||||
result=$(echo "in substitution")
|
||||
echo "result: $result"
|
||||
|
||||
#
|
||||
# DEBUG trap inheritance in pipelines
|
||||
#
|
||||
- name: "DEBUG trap - does not fire in pipeline without set -T"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
trap 'echo "[debug: $BASH_COMMAND]" >&2' DEBUG
|
||||
echo "before"
|
||||
echo "hello" | cat | cat
|
||||
echo "after"
|
||||
|
||||
- name: "DEBUG trap - fires in pipeline with set -T"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[debug: $BASH_COMMAND]" >&2' DEBUG
|
||||
echo "before"
|
||||
echo "hello" | cat | cat
|
||||
echo "after"
|
||||
|
||||
#
|
||||
# RETURN trap inheritance in functions
|
||||
#
|
||||
- name: "RETURN trap - does not fire in called function without set -T"
|
||||
stdin: |
|
||||
trap 'echo "[return: $FUNCNAME]"' RETURN
|
||||
inner() {
|
||||
echo "in inner"
|
||||
}
|
||||
outer() {
|
||||
echo "in outer"
|
||||
inner
|
||||
echo "outer continuing"
|
||||
}
|
||||
outer
|
||||
echo "after"
|
||||
|
||||
- name: "RETURN trap - fires in called function with set -T"
|
||||
known_failure: true # TODO(functrace): RETURN trap inheritance not implemented
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[return: ${FUNCNAME:-main}]"' RETURN
|
||||
inner() {
|
||||
echo "in inner"
|
||||
}
|
||||
outer() {
|
||||
echo "in outer"
|
||||
inner
|
||||
echo "outer continuing"
|
||||
}
|
||||
outer
|
||||
echo "after"
|
||||
|
||||
- name: "RETURN trap - fires for each nested function return with set -T"
|
||||
known_failure: true # TODO(functrace): RETURN trap inheritance not implemented
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[return: ${FUNCNAME:-main} -> $?]"' RETURN
|
||||
level3() { echo "level3"; return 3; }
|
||||
level2() { echo "level2"; level3; return 2; }
|
||||
level1() { echo "level1"; level2; return 1; }
|
||||
level1
|
||||
echo "done"
|
||||
|
||||
#
|
||||
# Function-specific trace attribute (declare -t)
|
||||
#
|
||||
- name: "declare -t - enables trace for specific function"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
traced_func() {
|
||||
echo "in traced"
|
||||
}
|
||||
untraced_func() {
|
||||
echo "in untraced"
|
||||
}
|
||||
declare -t traced_func
|
||||
echo "calling untraced"
|
||||
untraced_func
|
||||
echo "calling traced"
|
||||
traced_func
|
||||
echo "done"
|
||||
|
||||
- name: "declare +t - disables trace for specific function"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
myfunc() {
|
||||
echo "in func"
|
||||
}
|
||||
declare +t myfunc
|
||||
echo "calling func"
|
||||
myfunc
|
||||
echo "done"
|
||||
|
||||
- name: "declare -f - shows trace attribute"
|
||||
stdin: |
|
||||
myfunc() { echo hi; }
|
||||
declare -t myfunc
|
||||
declare -f myfunc | head -1
|
||||
|
||||
#
|
||||
# Toggling set -T mid-script
|
||||
#
|
||||
- name: "set -T enabled mid-script affects subsequent calls"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
func1() { echo "in func1"; }
|
||||
func2() { echo "in func2"; }
|
||||
echo "calling func1 without -T"
|
||||
func1
|
||||
set -T
|
||||
echo "calling func2 with -T"
|
||||
func2
|
||||
echo "done"
|
||||
|
||||
- name: "set +T disables inheritance mid-script"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[debug: $BASH_COMMAND]"' DEBUG
|
||||
func1() { echo "in func1"; }
|
||||
func2() { echo "in func2"; }
|
||||
echo "calling func1 with -T"
|
||||
func1
|
||||
set +T
|
||||
echo "calling func2 without -T"
|
||||
func2
|
||||
echo "done"
|
||||
|
||||
#
|
||||
# DEBUG trap inheritance in background jobs
|
||||
#
|
||||
- name: "DEBUG trap - in background job with set -T"
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[debug]" >&2' DEBUG
|
||||
{
|
||||
echo "in background"
|
||||
} &
|
||||
wait
|
||||
echo "done"
|
||||
|
||||
#
|
||||
# DEBUG trap inheritance in process substitution
|
||||
#
|
||||
- name: "DEBUG trap - in process substitution with set -T"
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[debug]" >&2' DEBUG
|
||||
cat <(echo "from procsub")
|
||||
echo "done"
|
||||
|
||||
#
|
||||
# BASH_COMMAND visibility in DEBUG trap with functrace
|
||||
#
|
||||
- name: "BASH_COMMAND in function with set -T"
|
||||
known_failure: true # TODO(BASH_COMMAND): quotes differ
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "cmd: $BASH_COMMAND"' DEBUG
|
||||
myfunc() {
|
||||
local v=10
|
||||
echo "v=$v"
|
||||
}
|
||||
myfunc
|
||||
|
||||
#
|
||||
# Nested functions with varying trace states
|
||||
#
|
||||
- name: "Outer function traced, inner function not traced"
|
||||
stdin: |
|
||||
trap 'echo "[debug in ${FUNCNAME:-main}]"' DEBUG
|
||||
inner() {
|
||||
echo "inner"
|
||||
}
|
||||
outer() {
|
||||
echo "outer start"
|
||||
inner
|
||||
echo "outer end"
|
||||
}
|
||||
declare -t outer
|
||||
outer
|
||||
echo "done"
|
||||
|
||||
- name: "Only inner function traced"
|
||||
stdin: |
|
||||
trap 'echo "[debug in ${FUNCNAME:-main}]"' DEBUG
|
||||
inner() {
|
||||
echo "inner"
|
||||
}
|
||||
outer() {
|
||||
echo "outer start"
|
||||
inner
|
||||
echo "outer end"
|
||||
}
|
||||
declare -t inner
|
||||
outer
|
||||
echo "done"
|
||||
|
||||
#
|
||||
# RETURN trap with sourced scripts and set -T
|
||||
#
|
||||
- name: "RETURN trap - fires for function in sourced script with set -T"
|
||||
known_failure: true # TODO(functrace): RETURN trap inheritance not implemented
|
||||
test_files:
|
||||
- path: "sourced.sh"
|
||||
contents: |
|
||||
sourced_func() {
|
||||
echo "in sourced_func"
|
||||
}
|
||||
sourced_func
|
||||
stdin: |
|
||||
set -T
|
||||
trap 'echo "[return: ${FUNCNAME:-main}]"' RETURN
|
||||
. ./sourced.sh
|
||||
echo "after source"
|
||||
|
||||
#
|
||||
# Edge cases
|
||||
#
|
||||
- name: "DEBUG trap - count in deeply nested function calls with set -T"
|
||||
known_failure: true # TODO(functrace): DEBUG trap count differs
|
||||
stdin: |
|
||||
set -T
|
||||
count=0
|
||||
trap 'count=$((count + 1))' DEBUG
|
||||
a() { b; }
|
||||
b() { c; }
|
||||
c() { echo "deep"; }
|
||||
a
|
||||
echo "debug count: $count"
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
name: "Options: inherit_errexit"
|
||||
cases:
|
||||
- name: "inherit_errexit disabled by default"
|
||||
stdin: |
|
||||
shopt -q inherit_errexit && echo "enabled" || echo "disabled"
|
||||
|
||||
- name: "inherit_errexit disabled - command substitution ignores errexit"
|
||||
stdin: |
|
||||
set -e
|
||||
shopt -u inherit_errexit
|
||||
result=$(false; echo "still running")
|
||||
echo "result: $result"
|
||||
echo "after"
|
||||
|
||||
- name: "inherit_errexit enabled - command substitution inherits errexit"
|
||||
stdin: |
|
||||
set -e
|
||||
shopt -s inherit_errexit
|
||||
result=$(false; echo "should not print")
|
||||
echo "should not print"
|
||||
|
||||
- name: "inherit_errexit with nested command substitution"
|
||||
stdin: |
|
||||
set -e
|
||||
shopt -s inherit_errexit
|
||||
result=$(echo "outer: $(false; echo inner)")
|
||||
echo "should not print"
|
||||
|
||||
- name: "inherit_errexit disabled - nested substitution continues"
|
||||
stdin: |
|
||||
set -e
|
||||
shopt -u inherit_errexit
|
||||
result=$(echo "outer: $(false; echo inner)")
|
||||
echo "result: $result"
|
||||
echo "after"
|
||||
|
||||
- name: "inherit_errexit with backtick substitution"
|
||||
stdin: |
|
||||
set -e
|
||||
shopt -s inherit_errexit
|
||||
result=`false; echo "should not print"`
|
||||
echo "should not print"
|
||||
|
||||
- name: "inherit_errexit with assignment"
|
||||
stdin: |
|
||||
set -e
|
||||
shopt -s inherit_errexit
|
||||
var=$(false)
|
||||
echo "should not print"
|
||||
|
||||
- name: "inherit_errexit in function"
|
||||
stdin: |
|
||||
set -e
|
||||
shopt -s inherit_errexit
|
||||
myfunc() {
|
||||
local result=$(false; echo "should not print")
|
||||
echo "after local"
|
||||
}
|
||||
myfunc
|
||||
echo "should not print"
|
||||
|
||||
- name: "inherit_errexit toggling"
|
||||
stdin: |
|
||||
set -e
|
||||
shopt -s inherit_errexit
|
||||
result1=$(false; echo "should not print in first")
|
||||
echo "should not print 1"
|
||||
ignore_stderr: true
|
||||
|
||||
- name: "inherit_errexit with multiple commands in substitution"
|
||||
stdin: |
|
||||
set -e
|
||||
shopt -u inherit_errexit
|
||||
result=$(echo "start"; false; echo "after false")
|
||||
echo "result: $result"
|
||||
echo "after"
|
||||
@@ -0,0 +1,9 @@
|
||||
name: "set -B"
|
||||
cases:
|
||||
- name: "set -B"
|
||||
stdin: |
|
||||
set +B
|
||||
echo "+B: " ${a,b}
|
||||
|
||||
set -B
|
||||
echo "-B: " ${a,b}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user