Skip to content

Commit 31921e6

Browse files
committed
feat(chat): support shell aliases in command execution
1 parent e0f65f3 commit 31921e6

File tree

5 files changed

+80
-27
lines changed

5 files changed

+80
-27
lines changed

codebase-summary.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ The chat implementation includes a robust tool system that allows Amazon Q to in
5757
1. **Available Tools**:
5858
- `fs_read`: Reads files or lists directories (similar to `cat` or `ls`)
5959
- `fs_write`: Creates or modifies files with various operations (create, append, replace)
60-
- `execute_bash`: Executes shell commands in the user's environment
60+
- `execute_shell_commands`: Executes shell commands in the user's environment
6161
- `use_aws`: Makes AWS CLI API calls with specified services and operations
6262

6363
2. **Tool Execution Flow**:
@@ -68,7 +68,7 @@ The chat implementation includes a robust tool system that allows Amazon Q to in
6868
- The conversation continues with the tool results incorporated
6969

7070
3. **Security Considerations**:
71-
- Tools that modify the system (like `fs_write` and `execute_bash`) require user confirmation
71+
- Tools that modify the system (like `fs_write` and `execute_shell_commands`) require user confirmation
7272
- The `/acceptall` command can toggle automatic acceptance for the session
7373
- Tool responses are limited to prevent excessive output (30KB limit)
7474

crates/q_cli/src/cli/chat/parser.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ mod tests {
332332
async fn test_parse() {
333333
let _ = tracing_subscriber::fmt::try_init();
334334
let tool_use_id = "TEST_ID".to_string();
335-
let tool_name = "execute_bash".to_string();
335+
let tool_name = "execute_shell_commands".to_string();
336336
let tool_args = serde_json::json!({
337337
"command": "echo hello"
338338
})

crates/q_cli/src/cli/chat/tools/execute_bash.rs renamed to crates/q_cli/src/cli/chat/tools/execute_shell_commands.rs

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::VecDeque;
2+
use std::fs;
23
use std::io::Write;
34
use std::process::Stdio;
45

@@ -27,11 +28,11 @@ use crate::cli::chat::truncate_safe;
2728
const READONLY_COMMANDS: &[&str] = &["ls", "cat", "echo", "pwd", "which", "head", "tail", "find", "grep"];
2829

2930
#[derive(Debug, Clone, Deserialize)]
30-
pub struct ExecuteBash {
31+
pub struct ExecuteShellCommands {
3132
pub command: String,
3233
}
3334

34-
impl ExecuteBash {
35+
impl ExecuteShellCommands {
3536
pub fn requires_acceptance(&self) -> bool {
3637
let Some(args) = shlex::split(&self.command) else {
3738
return true;
@@ -90,15 +91,63 @@ impl ExecuteBash {
9091
}
9192

9293
pub async fn invoke(&self, mut updates: impl Write) -> Result<InvokeOutput> {
94+
// Detect the user's default shell
95+
let shell = std::env::var("$0").unwrap_or_else(|_| "bash".to_string());
96+
97+
let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
98+
let shell_init_file = match shell.as_str() {
99+
s if s.contains("zsh") => Some(format!("{}/{}", home, ".zshrc")),
100+
s if s.contains("bash") => Some(format!("{}/{}", home, ".bashrc")),
101+
_ => None,
102+
};
103+
104+
// // Extract aliases from the shell config file
105+
let aliases = if let Some(config_path) = &shell_init_file {
106+
match fs::read_to_string(config_path) {
107+
Ok(content) => {
108+
// Extract aliases based on shell type
109+
let pattern = match shell.as_str() {
110+
s if s.contains("zsh") | s.contains("bash") => r#"^\s*alias\s+([^=]+)=['"]?([^'"]+)['"]?$"#,
111+
// TODO: support Fish and other shells
112+
_ => r"^\s*alias\s+.+$", // fallback, won't convert to function
113+
};
114+
115+
let re = regex::Regex::new(pattern).unwrap_or_else(|_| regex::Regex::new(r"").unwrap());
116+
let mut alias_commands = String::new();
117+
118+
for line in content.lines() {
119+
if let Some(caps) = re.captures(line) {
120+
if caps.len() >= 3 {
121+
let name = caps[1].trim();
122+
let raw_cmd = caps[2].trim();
123+
alias_commands.push_str(&format!(
124+
"{name}() {{ {cmd} \"$@\"; }}\n",
125+
name = name,
126+
cmd = raw_cmd
127+
));
128+
}
129+
}
130+
}
131+
132+
alias_commands
133+
},
134+
Err(_) => String::new(),
135+
}
136+
} else {
137+
String::new()
138+
};
139+
140+
let command_with_aliases = format!("{}\n{}", aliases, self.command);
141+
93142
// We need to maintain a handle on stderr and stdout, but pipe it to the terminal as well
94-
let mut child = tokio::process::Command::new("bash")
143+
let mut child = tokio::process::Command::new(&shell)
95144
.arg("-c")
96-
.arg(&self.command)
145+
.arg(&command_with_aliases)
97146
.stdin(Stdio::inherit())
98147
.stdout(Stdio::piped())
99148
.stderr(Stdio::piped())
100149
.spawn()
101-
.wrap_err_with(|| format!("Unable to spawn command '{}'", &self.command))?;
150+
.wrap_err_with(|| format!("Unable to spawn command '{}' in shell '{}'", &self.command, &shell))?;
102151

103152
let stdout = child.stdout.take().unwrap();
104153
let stdout = tokio::io::BufReader::new(stdout);
@@ -206,14 +255,14 @@ mod tests {
206255

207256
#[ignore = "todo: fix failing on musl for some reason"]
208257
#[tokio::test]
209-
async fn test_execute_bash_tool() {
258+
async fn test_execute_shell_commands_tool() {
210259
let mut stdout = std::io::stdout();
211260

212261
// Verifying stdout
213262
let v = serde_json::json!({
214263
"command": "echo Hello, world!",
215264
});
216-
let out = serde_json::from_value::<ExecuteBash>(v)
265+
let out = serde_json::from_value::<ExecuteShellCommands>(v)
217266
.unwrap()
218267
.invoke(&mut stdout)
219268
.await
@@ -231,7 +280,7 @@ mod tests {
231280
let v = serde_json::json!({
232281
"command": "echo Hello, world! 1>&2",
233282
});
234-
let out = serde_json::from_value::<ExecuteBash>(v)
283+
let out = serde_json::from_value::<ExecuteShellCommands>(v)
235284
.unwrap()
236285
.invoke(&mut stdout)
237286
.await
@@ -250,7 +299,7 @@ mod tests {
250299
"command": "exit 1",
251300
"interactive": false
252301
});
253-
let out = serde_json::from_value::<ExecuteBash>(v)
302+
let out = serde_json::from_value::<ExecuteShellCommands>(v)
254303
.unwrap()
255304
.invoke(&mut stdout)
256305
.await
@@ -304,7 +353,7 @@ mod tests {
304353
("find important-dir/ -name '*.txt'", false),
305354
];
306355
for (cmd, expected) in cmds {
307-
let tool = serde_json::from_value::<ExecuteBash>(serde_json::json!({
356+
let tool = serde_json::from_value::<ExecuteShellCommands>(serde_json::json!({
308357
"command": cmd,
309358
}))
310359
.unwrap();

crates/q_cli/src/cli/chat/tools/mod.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub mod execute_bash;
1+
pub mod execute_shell_commands;
22
pub mod fs_read;
33
pub mod fs_write;
44
pub mod gh_issue;
@@ -15,7 +15,7 @@ use aws_smithy_types::{
1515
Document,
1616
Number as SmithyNumber,
1717
};
18-
use execute_bash::ExecuteBash;
18+
use execute_shell_commands::ExecuteShellCommands;
1919
use eyre::Result;
2020
use fig_api_client::model::{
2121
ToolResult,
@@ -38,7 +38,7 @@ pub const MAX_TOOL_RESPONSE_SIZE: usize = 800000;
3838
pub enum Tool {
3939
FsRead(FsRead),
4040
FsWrite(FsWrite),
41-
ExecuteBash(ExecuteBash),
41+
ExecuteShellCommands(ExecuteShellCommands),
4242
UseAws(UseAws),
4343
GhIssue(GhIssue),
4444
}
@@ -49,7 +49,7 @@ impl Tool {
4949
match self {
5050
Tool::FsRead(_) => "Read from filesystem",
5151
Tool::FsWrite(_) => "Write to filesystem",
52-
Tool::ExecuteBash(_) => "Execute shell command",
52+
Tool::ExecuteShellCommands(_) => "Execute shell command",
5353
Tool::UseAws(_) => "Use AWS CLI",
5454
Tool::GhIssue(_) => "Prepare GitHub issue",
5555
}
@@ -60,7 +60,9 @@ impl Tool {
6060
match self {
6161
Tool::FsRead(_) => "Reading from filesystem",
6262
Tool::FsWrite(_) => "Writing to filesystem",
63-
Tool::ExecuteBash(execute_bash) => return format!("Executing `{}`", execute_bash.command),
63+
Tool::ExecuteShellCommands(execute_shell_commands) => {
64+
return format!("Executing `{}`", execute_shell_commands.command);
65+
},
6466
Tool::UseAws(_) => "Using AWS CLI",
6567
Tool::GhIssue(_) => "Preparing GitHub issue",
6668
}
@@ -72,7 +74,7 @@ impl Tool {
7274
match self {
7375
Tool::FsRead(_) => false,
7476
Tool::FsWrite(_) => true,
75-
Tool::ExecuteBash(execute_bash) => execute_bash.requires_acceptance(),
77+
Tool::ExecuteShellCommands(execute_shell_commands) => execute_shell_commands.requires_acceptance(),
7678
Tool::UseAws(use_aws) => use_aws.requires_acceptance(),
7779
Tool::GhIssue(_) => false,
7880
}
@@ -83,7 +85,7 @@ impl Tool {
8385
match self {
8486
Tool::FsRead(fs_read) => fs_read.invoke(context, updates).await,
8587
Tool::FsWrite(fs_write) => fs_write.invoke(context, updates).await,
86-
Tool::ExecuteBash(execute_bash) => execute_bash.invoke(updates).await,
88+
Tool::ExecuteShellCommands(execute_shell_commands) => execute_shell_commands.invoke(updates).await,
8789
Tool::UseAws(use_aws) => use_aws.invoke(context, updates).await,
8890
Tool::GhIssue(gh_issue) => gh_issue.invoke(updates).await,
8991
}
@@ -94,7 +96,7 @@ impl Tool {
9496
match self {
9597
Tool::FsRead(fs_read) => fs_read.queue_description(ctx, updates).await,
9698
Tool::FsWrite(fs_write) => fs_write.queue_description(ctx, updates),
97-
Tool::ExecuteBash(execute_bash) => execute_bash.queue_description(updates),
99+
Tool::ExecuteShellCommands(execute_shell_commands) => execute_shell_commands.queue_description(updates),
98100
Tool::UseAws(use_aws) => use_aws.queue_description(updates),
99101
Tool::GhIssue(gh_issue) => gh_issue.queue_description(updates),
100102
}
@@ -105,7 +107,7 @@ impl Tool {
105107
match self {
106108
Tool::FsRead(fs_read) => fs_read.validate(ctx).await,
107109
Tool::FsWrite(fs_write) => fs_write.validate(ctx).await,
108-
Tool::ExecuteBash(execute_bash) => execute_bash.validate(ctx).await,
110+
Tool::ExecuteShellCommands(execute_shell_commands) => execute_shell_commands.validate(ctx).await,
109111
Tool::UseAws(use_aws) => use_aws.validate(ctx).await,
110112
Tool::GhIssue(gh_issue) => gh_issue.validate(ctx).await,
111113
}
@@ -127,7 +129,9 @@ impl TryFrom<ToolUse> for Tool {
127129
Ok(match value.name.as_str() {
128130
"fs_read" => Self::FsRead(serde_json::from_value::<FsRead>(value.args).map_err(map_err)?),
129131
"fs_write" => Self::FsWrite(serde_json::from_value::<FsWrite>(value.args).map_err(map_err)?),
130-
"execute_bash" => Self::ExecuteBash(serde_json::from_value::<ExecuteBash>(value.args).map_err(map_err)?),
132+
"execute_shell_commands" => {
133+
Self::ExecuteShellCommands(serde_json::from_value::<ExecuteShellCommands>(value.args).map_err(map_err)?)
134+
},
131135
"use_aws" => Self::UseAws(serde_json::from_value::<UseAws>(value.args).map_err(map_err)?),
132136
"report_issue" => Self::GhIssue(serde_json::from_value::<GhIssue>(value.args).map_err(map_err)?),
133137
unknown => {

crates/q_cli/src/cli/chat/tools/tool_index.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
2-
"execute_bash": {
3-
"name": "execute_bash",
4-
"description": "Execute the specified bash command.",
2+
"execute_shell_commands": {
3+
"name": "execute_shell_commands",
4+
"description": "Execute the specified shell command.",
55
"input_schema": {
66
"type": "object",
77
"properties": {
88
"command": {
99
"type": "string",
10-
"description": "Bash command to execute"
10+
"description": "Shell command to execute"
1111
}
1212
},
1313
"required": [

0 commit comments

Comments
 (0)