Skip to content

Commit 8320e77

Browse files
CopilotPh0enixKM
andauthored
Implement sudo command modifier (#782)
* Initial plan * Implement sudo command modifier with detection logic and comprehensive tests Co-authored-by: Ph0enixKM <29208124+Ph0enixKM@users.noreply.github.com> * Fix sudo modifier to use runtime detection instead of compile-time detection Co-authored-by: Ph0enixKM <29208124+Ph0enixKM@users.noreply.github.com> * Refactor sudo implementation to use __sudo variable name and VarStmtFragment/VarExprFragment Co-authored-by: Ph0enixKM <29208124+Ph0enixKM@users.noreply.github.com> * Simplify sudo detection to use concise bash logic and return just VarExprFragment Co-authored-by: Ph0enixKM <29208124+Ph0enixKM@users.noreply.github.com> * Update sudo modifier error message for better consistency Co-authored-by: Ph0enixKM <29208124+Ph0enixKM@users.noreply.github.com> * Clean up code by removing comments from gen_sudo_prefix function Co-authored-by: Ph0enixKM <29208124+Ph0enixKM@users.noreply.github.com> * feat: update grammar ebnf * feat: properly test sudo * fix: typo * Remove else clause from chained modifiers sudo test as requested Co-authored-by: Ph0enixKM <29208124+Ph0enixKM@users.noreply.github.com> * Simplify sudo test condition by removing root user check Co-authored-by: Ph0enixKM <29208124+Ph0enixKM@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Ph0enixKM <29208124+Ph0enixKM@users.noreply.github.com> Co-authored-by: Phoenix Himself <pkaras.it@gmail.com>
1 parent 134ee60 commit 8320e77

File tree

6 files changed

+83
-16
lines changed

6 files changed

+83
-16
lines changed

grammar.ebnf

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ expression =
3636
builtins_expression |
3737
command |
3838
function_call |
39+
function_call_failed |
3940
identifier |
4041
list |
4142
null |
@@ -80,6 +81,7 @@ KEYWORD_REF = 'ref' ;
8081
KEYWORD_RETURN = 'return' ;
8182
KEYWORD_SILENT = 'silent' ;
8283
KEYWORD_STATUS = 'status' ;
84+
KEYWORD_SUDO = 'sudo' ;
8385
KEYWORD_SUCCEEDED = 'succeeded' ;
8486
KEYWORD_THEN = 'then' ;
8587
KEYWORD_TRUST = 'trust' ;
@@ -93,8 +95,6 @@ DIGIT = '0'..'9' ;
9395
TYPE = 'Text' | 'Num' | 'Bool' | 'Null';
9496
UNARY_OP = '-' | KEYWORD_NOT ;
9597
BINARY_OP = '+' | '-' | '*' | '/' | '%' | KEYWORD_AND | KEYWORD_OR | '==' | '!=' | '<' | '<=' | '>' | '>=' ;
96-
SILENT_MOD = KEYWORD_SILENT ;
97-
TRUST_MOD = KEYWORD_TRUST ;
9898
VISIBILITY = KEYWORD_PUB ;
9999
100100
(* Identifier *)
@@ -124,11 +124,10 @@ list = empty_list | full_list ;
124124
125125
(* Command expression *)
126126
(* The ordering of command modifiers doesn't matter *)
127-
command_modifier = SILENT_MOD, [ TRUST_MOD ] ;
127+
command_modifier = [ KEYWORD_SILENT ], [ KEYWORD_TRUST ], [ KEYWORD_SUDO ] ;
128128
command_modifier_block = command_modifier, multiline_block ;
129129
command_base = '$', { ANY_CHAR | interpolation }, '$' ;
130-
command = [ SILENT_MOD ], command_base, [ failure_handler | success_handler ] ;
131-
command_trust = [ SILENT_MOD ], TRUST_MOD, command_base ;
130+
command = command_modifier, command_base, [ failure_handler | success_handler ] ;
132131
133132
(* Operations *)
134133
binary_operation = expression, BINARY_OP, expression ;
@@ -154,10 +153,8 @@ variable_get = identifier, variable_index? ;
154153
variable_set = identifier, variable_index?, '=', expression ;
155154
156155
(* Function *)
157-
function_call = identifier, '(', [ expression, { ',', expression } ], ')' ;
158-
function_call_failed = [ SILENT_MOD ], function_call, failure_handler ;
159-
function_call_succeeded = [ SILENT_MOD ], function_call, success_handler ;
160-
function_call_trust = [ SILENT_MOD ], TRUST_MOD, function_call ;
156+
function_call = command_modifier, identifier, '(', [ expression, { ',', expression } ], ')' ;
157+
function_call_failed = function_call, [ failure_handler | success_handler ] ;
161158
function_def = [ VISIBILITY ], KEYWORD_FUN, identifier, '(', [ identifier, { ',', identifier } ], ')', block ;
162159
function_def_typed = [ VISIBILITY ], KEYWORD_FUN, identifier, '(',
163160
[ identifier, ':', TYPE, { ',', identifier, ':', TYPE } ], ')', ':', TYPE, block ;

src/modules/command/cmd.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use crate::modules::expression::interpolated_region::{InterpolatedRegionType, pa
88
use super::modifier::CommandModifier;
99
use heraclitus_compiler::prelude::*;
1010
use crate::modules::prelude::*;
11-
use crate::fragments;
1211

1312
#[derive(Debug, Clone)]
1413
pub struct Command {
@@ -90,7 +89,9 @@ impl Command {
9089
let succeeded = self.succeeded.translate(meta);
9190

9291
let mut is_silent = self.modifier.is_silent || meta.silenced;
92+
let mut is_sudo = self.modifier.is_sudo || meta.sudoed;
9393
swap(&mut is_silent, &mut meta.silenced);
94+
swap(&mut is_sudo, &mut meta.sudoed);
9495

9596
let translation = InterpolableFragment::new(
9697
self.strings.clone(),
@@ -99,8 +100,12 @@ impl Command {
99100
).to_frag();
100101

101102
let silent = meta.gen_silent().to_frag();
102-
let translation = fragments!(translation, silent);
103+
let sudo_prefix = meta.gen_sudo_prefix().to_frag();
104+
let translation = ListFragment::new(vec![sudo_prefix, translation, silent])
105+
.with_spaces()
106+
.to_frag();
103107
swap(&mut is_silent, &mut meta.silenced);
108+
swap(&mut is_sudo, &mut meta.sudoed);
104109

105110
// Choose between failed, succeeded, or no handler
106111
let handler = if self.failed.is_parsed {

src/modules/command/modifier.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ pub struct CommandModifier {
88
pub block: Box<Block>,
99
pub is_block: bool,
1010
pub is_trust: bool,
11-
pub is_silent: bool
11+
pub is_silent: bool,
12+
pub is_sudo: bool
1213
}
1314

1415
impl CommandModifier {
@@ -57,6 +58,13 @@ impl CommandModifier {
5758
self.is_silent = true;
5859
meta.increment_index();
5960
},
61+
"sudo" => {
62+
if self.is_sudo {
63+
return error!(meta, Some(tok.clone()), "Command modifier 'sudo' has already been declared");
64+
}
65+
self.is_sudo = true;
66+
meta.increment_index();
67+
},
6068
_ => break
6169
}
6270
},
@@ -75,7 +83,8 @@ impl SyntaxModule<ParserMetadata> for CommandModifier {
7583
block: Box::new(Block::new().with_no_indent()),
7684
is_block: true,
7785
is_trust: false,
78-
is_silent: false
86+
is_silent: false,
87+
is_sudo: false
7988
}
8089
}
8190

@@ -95,8 +104,10 @@ impl TranslateModule for CommandModifier {
95104
fn translate(&self, meta: &mut TranslateMetadata) -> FragmentKind {
96105
if self.is_block {
97106
meta.silenced = self.is_silent;
107+
meta.sudoed = self.is_sudo;
98108
let result = self.block.translate(meta);
99109
meta.silenced = false;
110+
meta.sudoed = false;
100111
result
101112
} else {
102113
FragmentKind::Empty
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Output
2+
// Command modifier 'sudo' has already been declared
3+
4+
main {
5+
// This should fail with duplicate sudo modifier error
6+
sudo sudo $ echo "test" $?
7+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Output
2+
// Succeeded
3+
4+
main {
5+
trust $ sudo() \{ command sudo -n "\$@"; } $
6+
7+
// Read first line of sudoers file
8+
sudo silent $ head -n1 /etc/sudoers $ failed {
9+
let code = status
10+
if {
11+
// Case when user is not a root
12+
code == 1 and trust $ id -u $ != "0":
13+
echo "Succeeded"
14+
// Case when sudo command is not installed
15+
code == 127:
16+
echo "Succeeded"
17+
else:
18+
echo "Unexpected behaviour: {code}"
19+
}
20+
exit
21+
}
22+
23+
if status == 0:
24+
echo "Succeeded"
25+
}

src/utils/metadata/translate.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::collections::VecDeque;
44
use super::ParserMetadata;
55
use crate::compiler::CompilerOptions;
66
use crate::modules::prelude::*;
7+
use crate::modules::types::Type;
8+
use crate::raw_fragment;
79
use crate::translate::compute::ArithType;
810
use crate::utils::function_cache::FunctionCache;
911
use crate::utils::function_metadata::FunctionMetadata;
@@ -26,6 +28,8 @@ pub struct TranslateMetadata {
2628
pub eval_ctx: bool,
2729
/// Determines whether the current context should be silenced.
2830
pub silenced: bool,
31+
/// Determines whether the current context should use sudo.
32+
pub sudoed: bool,
2933
/// The current indentation level.
3034
pub indent: i64,
3135
/// Determines if minify flag was set.
@@ -42,6 +46,7 @@ impl TranslateMetadata {
4246
value_id: 0,
4347
eval_ctx: false,
4448
silenced: false,
49+
sudoed: false,
4550
indent: -1,
4651
minify: options.minify,
4752
}
@@ -78,9 +83,26 @@ impl TranslateMetadata {
7883
id
7984
}
8085

81-
pub fn gen_silent(&self) -> RawFragment {
82-
let expr = if self.silenced { " >/dev/null 2>&1" } else { "" };
83-
RawFragment::new(expr)
86+
pub fn gen_silent(&self) -> FragmentKind {
87+
if self.silenced {
88+
raw_fragment!(">/dev/null 2>&1")
89+
} else {
90+
FragmentKind::Empty
91+
}
92+
}
93+
94+
pub fn gen_sudo_prefix(&mut self) -> FragmentKind {
95+
if self.sudoed {
96+
let var_name = "__sudo";
97+
let condition = r#"[ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1 && printf sudo"#;
98+
let condition_frag = RawFragment::new(&format!("$({})", condition)).to_frag();
99+
let var_stmt = VarStmtFragment::new(var_name, Type::Text, condition_frag);
100+
let var_expr = VarExprFragment::from_stmt(&var_stmt).with_quotes(false);
101+
self.stmt_queue.push_back(var_stmt.to_frag());
102+
var_expr.to_frag()
103+
} else {
104+
FragmentKind::Empty
105+
}
84106
}
85107

86108
// Returns the appropriate amount of quotes with escape symbols.

0 commit comments

Comments
 (0)